From 718fce068d2ab45e4476afd1a8ab377c89ac6484 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Wed, 13 Dec 2017 17:00:11 -0800 Subject: [PATCH] Refactor DashIopMpdNotifier Part 2 Move AdaptationSet related functions to the new Period class, which maps to element and provides methods to add AdaptationSets. Change-Id: I0fee290769fbe9a6355cc1b8c86baec8fbc4b4fd --- packager/mpd/base/adaptation_set.cc | 1 + packager/mpd/base/adaptation_set.h | 9 +- packager/mpd/base/adaptation_set_unittest.cc | 1 + packager/mpd/base/dash_iop_mpd_notifier.cc | 163 +--- packager/mpd/base/dash_iop_mpd_notifier.h | 58 +- .../base/dash_iop_mpd_notifier_unittest.cc | 797 ++---------------- packager/mpd/base/mock_mpd_builder.cc | 3 + packager/mpd/base/mock_mpd_builder.h | 16 +- packager/mpd/base/mpd_builder.cc | 88 +- packager/mpd/base/mpd_builder.h | 11 +- packager/mpd/base/mpd_builder_unittest.cc | 88 +- packager/mpd/base/period.cc | 237 ++++++ packager/mpd/base/period.h | 135 +++ packager/mpd/base/period_unittest.cc | 773 +++++++++++++++++ packager/mpd/base/representation.cc | 1 + packager/mpd/base/representation.h | 7 +- packager/mpd/base/representation_unittest.cc | 1 + packager/mpd/base/simple_mpd_notifier.cc | 21 +- packager/mpd/base/simple_mpd_notifier.h | 2 + .../mpd/base/simple_mpd_notifier_unittest.cc | 221 ++--- packager/mpd/mpd.gyp | 3 + ..._video_media_info1_expected_mpd_output.txt | 4 +- 22 files changed, 1457 insertions(+), 1183 deletions(-) create mode 100644 packager/mpd/base/period.cc create mode 100644 packager/mpd/base/period.h create mode 100644 packager/mpd/base/period_unittest.cc diff --git a/packager/mpd/base/adaptation_set.cc b/packager/mpd/base/adaptation_set.cc index b58e0cb555..3645d98483 100644 --- a/packager/mpd/base/adaptation_set.cc +++ b/packager/mpd/base/adaptation_set.cc @@ -12,6 +12,7 @@ #include "packager/base/strings/string_number_conversions.h" #include "packager/media/base/language_utils.h" #include "packager/mpd/base/media_info.pb.h" +#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/mpd_utils.h" #include "packager/mpd/base/representation.h" #include "packager/mpd/base/xml/xml_node.h" diff --git a/packager/mpd/base/adaptation_set.h b/packager/mpd/base/adaptation_set.h index 74cc5db8cc..b35c041a02 100644 --- a/packager/mpd/base/adaptation_set.h +++ b/packager/mpd/base/adaptation_set.h @@ -6,6 +6,9 @@ // /// All the methods that are virtual are virtual for mocking. +#ifndef PACKAGER_MPD_BASE_ADAPTATION_SET_H_ +#define PACKAGER_MPD_BASE_ADAPTATION_SET_H_ + #include #include @@ -15,7 +18,6 @@ #include #include "packager/base/atomic_sequence_num.h" -#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/xml/scoped_xml_ptr.h" namespace shaka { @@ -24,6 +26,7 @@ class MediaInfo; class Representation; struct ContentProtectionElement; +struct MpdOptions; namespace xml { class XmlNode; @@ -166,7 +169,7 @@ class AdaptationSet { AdaptationSet(const AdaptationSet&) = delete; AdaptationSet& operator=(const AdaptationSet&) = delete; - friend class MpdBuilder; + friend class Period; friend class AdaptationSetTest; // kSegmentAlignmentUnknown means that it is uncertain if the @@ -276,3 +279,5 @@ class AdaptationSet { }; } // namespace shaka + +#endif // PACKAGER_MPD_BASE_ADAPTATION_SET_H_ diff --git a/packager/mpd/base/adaptation_set_unittest.cc b/packager/mpd/base/adaptation_set_unittest.cc index f02b83a350..17224d1f80 100644 --- a/packager/mpd/base/adaptation_set_unittest.cc +++ b/packager/mpd/base/adaptation_set_unittest.cc @@ -10,6 +10,7 @@ #include #include "packager/mpd/base/content_protection_element.h" +#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/representation.h" #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" diff --git a/packager/mpd/base/dash_iop_mpd_notifier.cc b/packager/mpd/base/dash_iop_mpd_notifier.cc index ba93ed0a6c..ffea071216 100644 --- a/packager/mpd/base/dash_iop_mpd_notifier.cc +++ b/packager/mpd/base/dash_iop_mpd_notifier.cc @@ -10,36 +10,15 @@ #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/media_info.pb.h" #include "packager/mpd/base/mpd_notifier_util.h" -#include "packager/mpd/base/mpd_utils.h" +#include "packager/mpd/base/period.h" #include "packager/mpd/base/representation.h" -namespace shaka { - namespace { - -// 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) { - return content_protection1.SerializeAsString() == - content_protection2.SerializeAsString(); -} - -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; -} - +const bool kContentProtectionInAdaptationSet = true; } // namespace +namespace shaka { + DashIopMpdNotifier::DashIopMpdNotifier(const MpdOptions& mpd_options) : MpdNotifier(mpd_options), output_path_(mpd_options.mpd_params.mpd_output), @@ -63,7 +42,10 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, return false; base::AutoLock auto_lock(lock_); - AdaptationSet* adaptation_set = GetOrCreateAdaptationSet(media_info); + if (!period_) + period_ = mpd_builder_->AddPeriod(); + AdaptationSet* adaptation_set = period_->GetOrCreateAdaptationSet( + media_info, kContentProtectionInAdaptationSet); DCHECK(adaptation_set); MediaInfo adjusted_media_info(media_info); @@ -84,7 +66,7 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, bool DashIopMpdNotifier::NotifySampleDuration(uint32_t container_id, uint32_t sample_duration) { base::AutoLock auto_lock(lock_); - RepresentationMap::iterator it = representation_map_.find(container_id); + auto it = representation_map_.find(container_id); if (it == representation_map_.end()) { LOG(ERROR) << "Unexpected container_id: " << container_id; return false; @@ -98,7 +80,7 @@ bool DashIopMpdNotifier::NotifyNewSegment(uint32_t container_id, uint64_t duration, uint64_t size) { base::AutoLock auto_lock(lock_); - RepresentationMap::iterator it = representation_map_.find(container_id); + auto it = representation_map_.find(container_id); if (it == representation_map_.end()) { LOG(ERROR) << "Unexpected container_id: " << container_id; return false; @@ -113,7 +95,7 @@ bool DashIopMpdNotifier::NotifyEncryptionUpdate( const std::vector& new_key_id, const std::vector& new_pssh) { base::AutoLock auto_lock(lock_); - RepresentationMap::iterator it = representation_map_.find(container_id); + auto it = representation_map_.find(container_id); if (it == representation_map_.end()) { LOG(ERROR) << "Unexpected container_id: " << container_id; return false; @@ -141,127 +123,4 @@ bool DashIopMpdNotifier::Flush() { return WriteMpdToFile(output_path_, mpd_builder_.get()); } -AdaptationSet* DashIopMpdNotifier::GetOrCreateAdaptationSet( - const MediaInfo& media_info) { - const std::string key = GetAdaptationSetKey(media_info); - std::list& adaptation_sets = adaptation_set_list_map_[key]; - for (AdaptationSet* adaptation_set : adaptation_sets) { - if (protected_adaptation_set_map_.Match(*adaptation_set, media_info)) - return adaptation_set; - } - // None of the adaptation sets match with the new content protection. - // Need a new one. - AdaptationSet* new_adaptation_set = - NewAdaptationSet(media_info, adaptation_sets); - if (media_info.has_protected_content()) { - protected_adaptation_set_map_.Register(*new_adaptation_set, media_info); - AddContentProtectionElements(media_info, new_adaptation_set); - - // Set adaptation set switching. - for (AdaptationSet* adaptation_set : adaptation_sets) { - if (protected_adaptation_set_map_.Switchable(*adaptation_set, - *new_adaptation_set)) { - new_adaptation_set->AddAdaptationSetSwitching(adaptation_set->id()); - adaptation_set->AddAdaptationSetSwitching(new_adaptation_set->id()); - } - } - } - adaptation_sets.push_back(new_adaptation_set); - return new_adaptation_set; -} - -AdaptationSet* DashIopMpdNotifier::NewAdaptationSet( - const MediaInfo& media_info, - const std::list& adaptation_sets) { - std::string language = GetLanguage(media_info); - AdaptationSet* new_adaptation_set = mpd_builder_->AddAdaptationSet(language); - - if (media_info.has_video_info()) { - // Because 'lang' is ignored for videos, |adaptation_sets| must have - // all the video AdaptationSets. - if (adaptation_sets.size() > 1) { - new_adaptation_set->AddRole(AdaptationSet::kRoleMain); - } else if (adaptation_sets.size() == 1) { - // Set "main" Role for both AdaptatoinSets. - (*adaptation_sets.begin())->AddRole(AdaptationSet::kRoleMain); - new_adaptation_set->AddRole(AdaptationSet::kRoleMain); - } - - if (media_info.video_info().has_playback_rate()) { - uint32_t trick_play_reference_id = 0; - if (!FindOriginalAdaptationSetForTrickPlay(media_info, - &trick_play_reference_id)) { - LOG(ERROR) << "Failed to find main adaptation set for trick play."; - return nullptr; - } - DCHECK_NE(new_adaptation_set->id(), trick_play_reference_id); - new_adaptation_set->AddTrickPlayReferenceId(trick_play_reference_id); - } - } else if (media_info.has_text_info()) { - // IOP requires all AdaptationSets to have (sub)segmentAlignment set to - // true, so carelessly set it to true. - // In practice it doesn't really make sense to adapt between text tracks. - new_adaptation_set->ForceSetSegmentAlignment(true); - } - return new_adaptation_set; -} - -bool DashIopMpdNotifier::FindOriginalAdaptationSetForTrickPlay( - const MediaInfo& media_info, - uint32_t* main_adaptation_set_id) { - MediaInfo media_info_no_trickplay = media_info; - media_info_no_trickplay.mutable_video_info()->clear_playback_rate(); - - std::string key = GetAdaptationSetKey(media_info_no_trickplay); - const std::list& adaptation_sets = - adaptation_set_list_map_[key]; - for (AdaptationSet* adaptation_set : adaptation_sets) { - if (protected_adaptation_set_map_.Match(*adaptation_set, media_info)) { - *main_adaptation_set_id = adaptation_set->id(); - return true; - } - } - return false; -} - -void DashIopMpdNotifier::ProtectedAdaptationSetMap::Register( - const AdaptationSet& adaptation_set, - const MediaInfo& media_info) { - DCHECK(!ContainsKey(protected_content_map_, adaptation_set.id())); - protected_content_map_[adaptation_set.id()] = media_info.protected_content(); -} - -bool DashIopMpdNotifier::ProtectedAdaptationSetMap::Match( - const AdaptationSet& adaptation_set, - const MediaInfo& media_info) { - const auto protected_content_it = - protected_content_map_.find(adaptation_set.id()); - // If the AdaptationSet ID is not registered in the map, then it is clear - // content. - if (protected_content_it == protected_content_map_.end()) - return !media_info.has_protected_content(); - if (!media_info.has_protected_content()) - return false; - return ProtectedContentEq(protected_content_it->second, - media_info.protected_content()); -} - -bool DashIopMpdNotifier::ProtectedAdaptationSetMap::Switchable( - const AdaptationSet& adaptation_set_a, - const AdaptationSet& adaptation_set_b) { - const auto protected_content_it_a = - protected_content_map_.find(adaptation_set_a.id()); - const auto protected_content_it_b = - protected_content_map_.find(adaptation_set_b.id()); - - if (protected_content_it_a == protected_content_map_.end()) - return protected_content_it_b == protected_content_map_.end(); - if (protected_content_it_b == protected_content_map_.end()) - return false; - // Get all the UUIDs of the AdaptationSet. If another AdaptationSet has the - // same UUIDs then those are switchable. - return GetUUIDs(protected_content_it_a->second) == - GetUUIDs(protected_content_it_b->second); -} - } // namespace shaka diff --git a/packager/mpd/base/dash_iop_mpd_notifier.h b/packager/mpd/base/dash_iop_mpd_notifier.h index 93a3ed9134..64e627c460 100644 --- a/packager/mpd/base/dash_iop_mpd_notifier.h +++ b/packager/mpd/base/dash_iop_mpd_notifier.h @@ -9,7 +9,6 @@ #include "packager/mpd/base/mpd_notifier.h" -#include #include #include #include @@ -22,6 +21,7 @@ namespace shaka { class AdaptationSet; class MpdBuilder; +class Period; class Representation; /// This class is an MpdNotifier which will try its best to generate a @@ -59,31 +59,6 @@ class DashIopMpdNotifier : public MpdNotifier { private: friend class DashIopMpdNotifierTest; - // Maps representation ID to Representation. - typedef std::map RepresentationMap; - - // 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* GetOrCreateAdaptationSet(const MediaInfo& media_info); - - // Helper function to get a new AdaptationSet and set new AdaptationSet - // attributes. - AdaptationSet* NewAdaptationSet( - const MediaInfo& media_info, - const std::list& adaptation_sets); - - // Gets the original AdaptationSet which the trick play video belongs - // to and returns the id of the original adapatation set. - // It is assumed that the corresponding AdaptationSet has been created before - // the trick play AdaptationSet. - // Returns true if main_adaptation_id is found, otherwise false; - bool FindOriginalAdaptationSetForTrickPlay( - const MediaInfo& media_info, - uint32_t* original_adaptation_set_id); - // Testing only method. Returns a pointer to MpdBuilder. MpdBuilder* MpdBuilderForTesting() const { return mpd_builder_.get(); @@ -94,39 +69,14 @@ class DashIopMpdNotifier : public MpdNotifier { mpd_builder_ = std::move(mpd_builder); } - std::map> adaptation_set_list_map_; - RepresentationMap representation_map_; - - // Tracks ProtectedContent in AdaptationSet. - class ProtectedAdaptationSetMap { - public: - ProtectedAdaptationSetMap() = default; - // Register the |adaptation_set| with associated |media_info| in the map. - void Register(const AdaptationSet& adaptation_set, - const MediaInfo& media_info); - // Check if the protected content associated with |adaptation_set| matches - // with the one in |media_info|. - bool Match(const AdaptationSet& adaptation_set, - const MediaInfo& media_info); - // Check if the two adaptation sets are switchable. - bool Switchable(const AdaptationSet& adaptation_set_a, - const AdaptationSet& adaptation_set_b); - - private: - ProtectedAdaptationSetMap(const ProtectedAdaptationSetMap&) = delete; - ProtectedAdaptationSetMap& operator=(const ProtectedAdaptationSetMap&) = - delete; - - // Maps AdaptationSet ID to ProtectedContent. - std::map protected_content_map_; - }; - ProtectedAdaptationSetMap protected_adaptation_set_map_; - // MPD output path. std::string output_path_; std::unique_ptr mpd_builder_; + Period* period_ = nullptr; // owned by |mpd_builder_|. base::Lock lock_; + // Maps representation ID to Representation. + std::map representation_map_; // Maps Representation ID to AdaptationSet. This is for updating the PSSH. std::map representation_id_to_adaptation_set_; }; diff --git a/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc index 43a40279ae..5d08f9f303 100644 --- a/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc +++ b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc @@ -5,6 +5,7 @@ // https://developers.google.com/open-source/licenses/bsd #include +#include #include #include "packager/base/files/file_path.h" @@ -17,70 +18,19 @@ namespace shaka { using ::testing::_; +using ::testing::AnyOf; using ::testing::Eq; -using ::testing::ElementsAre; -using ::testing::InSequence; using ::testing::Return; using ::testing::StrEq; -using ::testing::UnorderedElementsAre; 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 bool kContentProtectionInAdaptationSet = true; -bool ElementEqual(const Element& lhs, const 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 ContentProtectionElement& lhs, - const 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); +MATCHER_P(EqualsProto, message, "") { + return ::google::protobuf::util::MessageDifferencer::Equals(arg, message); } } // namespace @@ -94,7 +44,8 @@ MATCHER_P(ContentProtectionElementEq, expected, "") { class DashIopMpdNotifierTest : public ::testing::Test { protected: DashIopMpdNotifierTest() - : default_mock_adaptation_set_( + : default_mock_period_(new MockPeriod), + default_mock_adaptation_set_( new MockAdaptationSet(kDefaultAdaptationSetId)), default_mock_representation_( new MockRepresentation(kDefaultRepresentationId)) {} @@ -102,6 +53,24 @@ class DashIopMpdNotifierTest : public ::testing::Test { void SetUp() override { ASSERT_TRUE(base::CreateTemporaryFile(&temp_file_path_)); empty_mpd_option_.mpd_params.mpd_output = temp_file_path_.AsUTF8Unsafe(); + + // Three valid media info. The actual data does not matter. + 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"; + valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo); + valid_media_info2_ = valid_media_info1_; + valid_media_info2_.mutable_video_info()->set_width(960); + valid_media_info3_ = valid_media_info1_; + valid_media_info3_.mutable_video_info()->set_width(480); } void TearDown() override { @@ -113,16 +82,23 @@ class DashIopMpdNotifierTest : public ::testing::Test { notifier->SetMpdBuilderForTesting(std::move(mpd_builder)); } + protected: // Empty mpd options except with output path specified, so that // WriteMpdToFile() doesn't crash. MpdOptions empty_mpd_option_; // Default mocks that can be used for the tests. // IOW, if a test only requires one instance of - // Mock{AdaptationSet,Representation}, these can be used. + // Mock{Period,AdaptationSet,Representation}, these can be used. + std::unique_ptr default_mock_period_; std::unique_ptr default_mock_adaptation_set_; std::unique_ptr default_mock_representation_; + // Three valid media info. The actual content does not matter. + MediaInfo valid_media_info1_; + MediaInfo valid_media_info2_; + MediaInfo valid_media_info3_; + private: base::FilePath temp_file_path_; }; @@ -134,9 +110,12 @@ TEST_F(DashIopMpdNotifierTest, NotifyNewContainer) { std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), + Eq(kContentProtectionInAdaptationSet))) .WillOnce(Return(default_mock_adaptation_set_.get())); - EXPECT_CALL(*default_mock_adaptation_set_, AddRole(_)).Times(0); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(default_mock_representation_.get())); @@ -146,616 +125,48 @@ TEST_F(DashIopMpdNotifierTest, NotifyNewContainer) { uint32_t unused_container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info1_, &unused_container_id)); EXPECT_TRUE(notifier.Flush()); } -// Verify that basic VOD NotifyNewContainer() operation works on trick play -// streams. -// No encrypted contents. -TEST_F(DashIopMpdNotifierTest, NotifyNewContainerForTrickPlay) { - const char kTrickPlayMediaInfo[] = - "video_info {\n" - " codec: 'avc1'\n" - " width: 1280\n" - " height: 720\n" - " time_scale: 10\n" - " frame_duration: 100\n" - " pixel_width: 1\n" - " pixel_height: 1\n" - " playback_rate: 10\n" - "}\n" - "container_type: 1\n"; - DashIopMpdNotifier notifier(empty_mpd_option_); - - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - // Not using default mocks in this test so that we can keep track of - // mocks by named mocks. - const uint32_t kAdaptationSetId = 2u; - const uint32_t kTrickPlayAdaptationSetId = 3u; - std::unique_ptr mock_adaptation_set( - new MockAdaptationSet(kAdaptationSetId)); - std::unique_ptr mock_tp_adaptation_set( - new MockAdaptationSet(kTrickPlayAdaptationSetId)); - - const uint32_t kRepresentationId = 4u; - const uint32_t kTrickPlayRepresentationId = 5u; - std::unique_ptr mock_representation( - new MockRepresentation(kRepresentationId)); - std::unique_ptr mock_tp_representation( - new MockRepresentation(kTrickPlayRepresentationId)); - - InSequence in_sequence; - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) - .WillOnce(Return(mock_adaptation_set.get())); - EXPECT_CALL(*mock_adaptation_set, AddRole(_)).Times(0); - EXPECT_CALL(*mock_adaptation_set, AddRepresentation(_)) - .WillOnce(Return(mock_representation.get())); - - // Calls for trick-play stream. - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) - .WillOnce(Return(mock_tp_adaptation_set.get())); - EXPECT_CALL(*mock_tp_adaptation_set, AddRole(_)).Times(0); - EXPECT_CALL(*mock_tp_adaptation_set, - AddTrickPlayReferenceId(kAdaptationSetId)) - .Times(1); - EXPECT_CALL(*mock_tp_adaptation_set, AddRepresentation(_)) - .WillOnce(Return(mock_tp_representation.get())); - - EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); - - uint32_t unused_container_id; - SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kTrickPlayMediaInfo), &unused_container_id)); - EXPECT_TRUE(notifier.Flush()); -} - -// Verify that if the MediaInfo contains text information, then -// MpdBuilder::ForceSetSegmentAlignment() is called. -TEST_F(DashIopMpdNotifierTest, NotifyNewTextContainer) { - const char kTextMediaInfo[] = - "text_info {\n" - " format: 'ttml'\n" - " language: 'en'\n" - "}\n" - "container_type: CONTAINER_TEXT\n"; - DashIopMpdNotifier notifier(empty_mpd_option_); - - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(StrEq("en"))) - .WillOnce(Return(default_mock_adaptation_set_.get())); - EXPECT_CALL(*default_mock_adaptation_set_, AddRole(_)).Times(0); - EXPECT_CALL(*default_mock_adaptation_set_, ForceSetSegmentAlignment(true)); - EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) - .WillOnce(Return(default_mock_representation_.get())); - - // This is for the Flush() below but adding expectation here because the next - // std::move(lines) the pointer. - EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); - - uint32_t unused_container_id; - SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kTextMediaInfo), - &unused_container_id)); - EXPECT_TRUE(notifier.Flush()); -} - -// Verify VOD NotifyNewContainer() operation works with different -// MediaInfo::ProtectedContent. -// Two AdaptationSets should be created. -// AdaptationSets with different DRM won't be switchable. -TEST_F(DashIopMpdNotifierTest, - NotifyNewContainersWithDifferentProtectedContent) { - DashIopMpdNotifier notifier(empty_mpd_option_); - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - // 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; - std::unique_ptr sd_adaptation_set( - new MockAdaptationSet(kSdAdaptationSetId)); - std::unique_ptr hd_adaptation_set( - new MockAdaptationSet(kHdAdaptationSetId)); - - const uint32_t kSdRepresentation = 4u; - const uint32_t kHdRepresentation = 5u; - std::unique_ptr sd_representation( - new MockRepresentation(kSdRepresentation)); - std::unique_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())); - - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) - .WillOnce(Return(hd_adaptation_set.get())); - - // Add main Role here for both. - EXPECT_CALL(*sd_adaptation_set, AddRole(AdaptationSet::kRoleMain)); - EXPECT_CALL(*hd_adaptation_set, AddRole(AdaptationSet::kRoleMain)); - - // 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())); - - uint32_t unused_container_id; - SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kHdProtectedContent), &unused_container_id)); - - EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(), ElementsAre()); - EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(), ElementsAre()); -} - -// Verify VOD NotifyNewContainer() operation works with same -// MediaInfo::ProtectedContent. Only one AdaptationSet should be -// created. -TEST_F(DashIopMpdNotifierTest, NotifyNewContainersWithSameProtectedContent) { - DashIopMpdNotifier notifier(empty_mpd_option_); - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - // 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; - std::unique_ptr sd_representation( - new MockRepresentation(kSdRepresentation)); - std::unique_ptr hd_representation( - new MockRepresentation(kHdRepresentation)); - - 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_, AddRole(_)).Times(0); - EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) - .WillOnce(Return(sd_representation.get())); - - // 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_, AddRole(_)).Times(0); - EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) - .WillOnce(Return(hd_representation.get())); - - uint32_t unused_container_id; - SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kHdProtectedContent), &unused_container_id)); - - // No adaptation set switching if there is only one AdaptationSet. - EXPECT_THAT(default_mock_adaptation_set_->adaptation_set_switching_ids(), - ElementsAre()); -} - // AddContentProtection() should not work and should always return false. TEST_F(DashIopMpdNotifierTest, AddContentProtection) { DashIopMpdNotifier notifier(empty_mpd_option_); std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(default_mock_representation_.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &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 -// the AdaptationSet they belong to should be switchable. -// This is a long test. -// Basically this -// 1. Add an SD protected content. This should make an AdaptationSet. -// 2. Add an HD protected content. This should make another AdaptationSet that -// is different from the SD version. SD AdaptationSet and HD AdaptationSet -// should be switchable. -// 3. Add a 4k protected content. This should also make a new AdaptationSet. -// It should be switchable with SD/HD AdaptationSet. -TEST_F(DashIopMpdNotifierTest, SetAdaptationSetSwitching) { - DashIopMpdNotifier notifier(empty_mpd_option_); - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - // 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; - std::unique_ptr sd_adaptation_set( - new MockAdaptationSet(kSdAdaptationSetId)); - std::unique_ptr hd_adaptation_set( - new MockAdaptationSet(kHdAdaptationSetId)); - - const uint32_t kSdRepresentation = 4u; - const uint32_t kHdRepresentation = 5u; - std::unique_ptr sd_representation( - new MockRepresentation(kSdRepresentation)); - std::unique_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())); - - 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())); - - // 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, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kSdProtectedContent), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kHdProtectedContent), &unused_container_id)); - - EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(), - ElementsAre(kHdAdaptationSetId)); - EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(), - ElementsAre(kSdAdaptationSetId)); - - // Add another content that has the same protected content and make sure that - // adaptation set switching is set correctly. - 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; - std::unique_ptr fourk_adaptation_set( - new MockAdaptationSet(k4kAdaptationSetId)); - - const uint32_t k4kRepresentationId = 4001u; - std::unique_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())); - - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(k4kProtectedContent), &unused_container_id)); - - EXPECT_THAT(sd_adaptation_set->adaptation_set_switching_ids(), - UnorderedElementsAre(kHdAdaptationSetId, k4kAdaptationSetId)); - EXPECT_THAT(hd_adaptation_set->adaptation_set_switching_ids(), - UnorderedElementsAre(kSdAdaptationSetId, k4kAdaptationSetId)); - EXPECT_THAT(fourk_adaptation_set->adaptation_set_switching_ids(), - ElementsAre(kSdAdaptationSetId, kHdAdaptationSetId)); -} - -// Even if the UUIDs match, video and audio AdaptationSets should not be -// switchable. -TEST_F(DashIopMpdNotifierTest, - DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) { - DashIopMpdNotifier notifier(empty_mpd_option_); - std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - - // 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 kVideoAdaptationSetId = 6u; - const uint32_t kAudioAdaptationSetId = 7u; - std::unique_ptr video_adaptation_set( - new MockAdaptationSet(kVideoAdaptationSetId)); - std::unique_ptr audio_adaptation_set( - new MockAdaptationSet(kAudioAdaptationSetId)); - - const uint32_t kVideoRepresentation = 8u; - const uint32_t kAudioRepresentation = 9u; - std::unique_ptr video_representation( - new MockRepresentation(kVideoRepresentation)); - std::unique_ptr audio_representation( - new MockRepresentation(kAudioRepresentation)); - - 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, AddRole(_)).Times(0); - EXPECT_CALL(*video_adaptation_set, AddRepresentation(_)) - .WillOnce(Return(video_representation.get())); - - 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, AddRole(_)).Times(0); - EXPECT_CALL(*audio_adaptation_set, AddRepresentation(_)) - .WillOnce(Return(audio_representation.get())); - - uint32_t unused_container_id; - SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kVideoContent), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent), &unused_container_id)); - - EXPECT_THAT(video_adaptation_set->adaptation_set_switching_ids(), - ElementsAre()); - EXPECT_THAT(audio_adaptation_set->adaptation_set_switching_ids(), - ElementsAre()); -} - TEST_F(DashIopMpdNotifierTest, UpdateEncryption) { - const char kProtectedContent[] = - "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"; - DashIopMpdNotifier notifier(empty_mpd_option_); std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); - EXPECT_CALL(*default_mock_adaptation_set_, AddRole(_)).Times(0); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(default_mock_representation_.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kProtectedContent), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); ::testing::Mock::VerifyAndClearExpectations( default_mock_adaptation_set_.get()); @@ -783,73 +194,19 @@ TEST_F(DashIopMpdNotifierTest, UpdateEncryption) { TEST_F(DashIopMpdNotifierTest, NotifyNewContainerAndSampleDurationNoMock) { DashIopMpdNotifier notifier(empty_mpd_option_); uint32_t container_id; - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); const uint32_t kAnySampleDuration = 1000; EXPECT_TRUE(notifier.NotifySampleDuration(container_id, kAnySampleDuration)); EXPECT_TRUE(notifier.Flush()); } -// Don't put different audio languages or codecs in the same AdaptationSet. -TEST_F(DashIopMpdNotifierTest, SplitAdaptationSetsByLanguageAndCodec) { - // MP4, English - const char kAudioContent1[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'eng'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - // MP4, German - const char kAudioContent2[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - // WebM, German - const char kAudioContent3[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - - // WebM, German again - const char kAudioContent4[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - +// Test multiple media info with some belongs to the same AdaptationSets. +TEST_F(DashIopMpdNotifierTest, MultipleMediaInfo) { DashIopMpdNotifier notifier(empty_mpd_option_); std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); std::unique_ptr adaptation_set1(new MockAdaptationSet(1)); std::unique_ptr adaptation_set2(new MockAdaptationSet(2)); - std::unique_ptr adaptation_set3(new MockAdaptationSet(3)); std::unique_ptr representation1( new MockRepresentation(1)); @@ -857,35 +214,39 @@ TEST_F(DashIopMpdNotifierTest, SplitAdaptationSetsByLanguageAndCodec) { new MockRepresentation(2)); std::unique_ptr representation3( new MockRepresentation(3)); - std::unique_ptr representation4( - new MockRepresentation(4)); - // We expect three AdaptationSets. - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) - .WillOnce(Return(adaptation_set1.get())) - .WillOnce(Return(adaptation_set2.get())) - .WillOnce(Return(adaptation_set3.get())); - // The first AdaptationSet should have Eng MP4, one Representation. - EXPECT_CALL(*adaptation_set1, AddRepresentation(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) + .WillOnce(Return(adaptation_set1.get())); + EXPECT_CALL(*adaptation_set1, + AddRepresentation(EqualsProto(valid_media_info1_))) .WillOnce(Return(representation1.get())); - // The second AdaptationSet should have Ger MP4, one Representation. - EXPECT_CALL(*adaptation_set2, AddRepresentation(_)) - .WillOnce(Return(representation2.get())); - // The third AdaptationSet should have Ger WebM, two Representations. - EXPECT_CALL(*adaptation_set3, AddRepresentation(_)) - .WillOnce(Return(representation3.get())) - .WillOnce(Return(representation4.get())); + // Return the same adaptation set for |valid_media_info2_| and + // |valid_media_info3_|. This results in AddRepresentation to be called twice + // on |adaptation_set2|. + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(AnyOf(EqualsProto(valid_media_info2_), + EqualsProto(valid_media_info3_)), + _)) + .WillOnce(Return(adaptation_set2.get())) + .WillOnce(Return(adaptation_set2.get())); + EXPECT_CALL(*adaptation_set2, + AddRepresentation(AnyOf(EqualsProto(valid_media_info2_), + EqualsProto(valid_media_info3_)))) + .WillOnce(Return(representation2.get())) + .WillOnce(Return(representation3.get())); uint32_t unused_container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent1), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent2), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent3), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent4), &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info1_, &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info2_, &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info3_, &unused_container_id)); } } // namespace shaka diff --git a/packager/mpd/base/mock_mpd_builder.cc b/packager/mpd/base/mock_mpd_builder.cc index 09a851bc8e..35f186dfb9 100644 --- a/packager/mpd/base/mock_mpd_builder.cc +++ b/packager/mpd/base/mock_mpd_builder.cc @@ -13,6 +13,9 @@ const MpdOptions kDefaultMpdOptions; MockMpdBuilder::MockMpdBuilder() : MpdBuilder(kDefaultMpdOptions) {} MockMpdBuilder::~MockMpdBuilder() {} +MockPeriod::MockPeriod() + : Period(kDefaultMpdOptions, &sequence_counter_, &sequence_counter_) {} + MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id) : AdaptationSet(adaptation_set_id, kEmptyLang, diff --git a/packager/mpd/base/mock_mpd_builder.h b/packager/mpd/base/mock_mpd_builder.h index 84d30442b4..49a402e512 100644 --- a/packager/mpd/base/mock_mpd_builder.h +++ b/packager/mpd/base/mock_mpd_builder.h @@ -14,6 +14,7 @@ #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/content_protection_element.h" #include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/base/period.h" #include "packager/mpd/base/representation.h" namespace shaka { @@ -23,10 +24,23 @@ class MockMpdBuilder : public MpdBuilder { MockMpdBuilder(); ~MockMpdBuilder() override; - MOCK_METHOD1(AddAdaptationSet, AdaptationSet*(const std::string& lang)); + MOCK_METHOD0(AddPeriod, Period*()); MOCK_METHOD1(ToString, bool(std::string* output)); }; +class MockPeriod : public Period { + public: + MockPeriod(); + + MOCK_METHOD2(GetOrCreateAdaptationSet, + AdaptationSet*(const MediaInfo& media_info, + bool content_protection_in_adaptation_set)); + + private: + // Only for constructing the super class. Not used for testing. + base::AtomicSequenceNumber sequence_counter_; +}; + class MockAdaptationSet : public AdaptationSet { public: // |adaptation_set_id| is the id for the AdaptationSet. diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index 48a1a93f4c..3f1a623ec4 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -15,6 +15,7 @@ #include "packager/base/time/time.h" #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/mpd_utils.h" +#include "packager/mpd/base/period.h" #include "packager/mpd/base/xml/xml_node.h" #include "packager/version/version.h" @@ -139,18 +140,10 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) { base_urls_.push_back(base_url); } -AdaptationSet* MpdBuilder::AddAdaptationSet(const std::string& lang) { - std::unique_ptr adaptation_set( - new AdaptationSet(adaptation_set_counter_.GetNext(), lang, mpd_options_, - &representation_counter_)); - DCHECK(adaptation_set); - - if (!lang.empty() && lang == mpd_options_.mpd_params.default_language) { - adaptation_set->AddRole(AdaptationSet::kRoleMain); - } - - adaptation_sets_.push_back(std::move(adaptation_set)); - return adaptation_sets_.back().get(); +Period* MpdBuilder::AddPeriod() { + periods_.emplace_back(new Period(mpd_options_, &adaptation_set_counter_, + &representation_counter_)); + return periods_.back().get(); } bool MpdBuilder::ToString(std::string* output) { @@ -158,7 +151,7 @@ bool MpdBuilder::ToString(std::string* output) { static LibXmlInitializer lib_xml_initializer; xml::scoped_xml_ptr doc(GenerateMpd()); - if (!doc.get()) + if (!doc) return false; static const int kNiceFormat = 1; @@ -180,39 +173,21 @@ xmlDocPtr MpdBuilder::GenerateMpd() { xml::scoped_xml_ptr doc(xmlNewDoc(BAD_CAST kXmlVersion)); XmlNode mpd("MPD"); - // Iterate thru AdaptationSets and add them to one big Period element. - XmlNode period("Period"); - - // Always set id=0 for now. Since this class can only generate one Period - // at the moment, just use a constant. - // Required for 'dynamic' MPDs. - period.SetId(0); - for (const std::unique_ptr& adaptation_set : - adaptation_sets_) { - xml::scoped_xml_ptr child(adaptation_set->GetXml()); - if (!child.get() || !period.AddChild(std::move(child))) - return NULL; - } - // Add baseurls to MPD. - std::list::const_iterator base_urls_it = base_urls_.begin(); - for (; base_urls_it != base_urls_.end(); ++base_urls_it) { - XmlNode base_url("BaseURL"); - base_url.SetContent(*base_urls_it); + for (const std::string& base_url : base_urls_) { + XmlNode xml_base_url("BaseURL"); + xml_base_url.SetContent(base_url); - if (!mpd.AddChild(base_url.PassScopedPtr())) - return NULL; + if (!mpd.AddChild(xml_base_url.PassScopedPtr())) + return nullptr; } - // TODO(kqyang): Should we set @start unconditionally to 0? - if (mpd_options_.mpd_type == MpdType::kDynamic) { - // This is the only Period and it is a regular period. - period.SetStringAttribute("start", "PT0S"); + for (const auto& period : periods_) { + xml::scoped_xml_ptr period_node(period->GetXml()); + if (!period_node || !mpd.AddChild(std::move(period_node))) + return nullptr; } - if (!mpd.AddChild(period.PassScopedPtr())) - return NULL; - AddMpdNameSpaceInfo(&mpd); static const char kOnDemandProfile[] = @@ -331,15 +306,21 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { DCHECK(mpd_node); DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type); - xmlNodePtr period_node = FindPeriodNode(mpd_node); - DCHECK(period_node) << "Period element must be a child of mpd_node."; - DCHECK(IsPeriodNode(period_node)); - - // TODO(kqyang): Verify if this works for static + live profile. // Attribute mediaPresentationDuration must be present for 'static' MPD. So // setting "PT0S" is required even if none of the representaions have duration // attribute. float max_duration = 0.0f; + + xmlNodePtr period_node = FindPeriodNode(mpd_node); + if (!period_node) { + LOG(WARNING) << "No Period node found. Set MPD duration to 0."; + return 0.0f; + } + + DCHECK(IsPeriodNode(period_node)); + // TODO(kqyang): Why don't we iterate the C++ classes instead of iterating XML + // elements? + // TODO(kqyang): Verify if this works for static + live profile. for (xmlNodePtr adaptation_set = xmlFirstElementChild(period_node); adaptation_set; adaptation_set = xmlNextElementSibling(adaptation_set)) { for (xmlNodePtr representation = xmlFirstElementChild(adaptation_set); @@ -361,21 +342,8 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) { DCHECK(timestamp_seconds); - - double earliest_timestamp(-1); - for (const std::unique_ptr& adaptation_set : - adaptation_sets_) { - double timestamp; - if (adaptation_set->GetEarliestTimestamp(×tamp) && - ((earliest_timestamp < 0) || (timestamp < earliest_timestamp))) { - earliest_timestamp = timestamp; - } - } - if (earliest_timestamp < 0) - return false; - - *timestamp_seconds = earliest_timestamp; - return true; + DCHECK(!periods_.empty()); + return periods_.front()->GetEarliestTimestamp(timestamp_seconds); } void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path, diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index 22716d6e61..5cb1b23ac1 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -28,6 +28,7 @@ namespace shaka { class AdaptationSet; class MediaInfo; +class Period; namespace xml { class XmlNode; @@ -45,11 +46,9 @@ class MpdBuilder { /// @param base_url URL for entry. void AddBaseUrl(const std::string& base_url); - /// Adds to the MPD. - /// @param lang is the language of the AdaptationSet. This can be empty for - /// videos, for example. - /// @return The new adaptation set, which is owned by this instance. - virtual AdaptationSet* AddAdaptationSet(const std::string& lang); + /// Adds to the MPD. + /// @return The new period, which is owned by this instance. + virtual Period* AddPeriod(); /// Writes the MPD to the given string. /// @param[out] output is an output string where the MPD gets written. @@ -108,7 +107,7 @@ class MpdBuilder { bool GetEarliestTimestamp(double* timestamp_seconds); MpdOptions mpd_options_; - std::list> adaptation_sets_; + std::list> periods_; std::list base_urls_; std::string availability_start_time_; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index ae82ac8b2c..a9a46ea3e1 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -8,6 +8,7 @@ #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/base/period.h" #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/version/version.h" @@ -35,6 +36,11 @@ class MpdBuilderTest : public ::testing::Test { } ~MpdBuilderTest() override {} + void SetUp() override { + period_ = mpd_.AddPeriod(); + ASSERT_TRUE(period_); + } + MpdOptions* mutable_mpd_options() { return &mpd_.mpd_options_; } void CheckMpd(const std::string& expected_output_file) { @@ -50,7 +56,8 @@ class MpdBuilderTest : public ::testing::Test { // Creates a new AdaptationSet and adds a Representation element using // |media_info|. void AddRepresentation(const MediaInfo& media_info) { - AdaptationSet* adaptation_set = mpd_.AddAdaptationSet(""); + AdaptationSet* adaptation_set = + period_->GetOrCreateAdaptationSet(media_info, true); ASSERT_TRUE(adaptation_set); Representation* representation = @@ -66,6 +73,7 @@ class MpdBuilderTest : public ::testing::Test { Representation* representation_; // Owned by |mpd_|. private: + Period* period_ = nullptr; base::AtomicSequenceNumber representation_counter_; DISALLOW_COPY_AND_ASSIGN(MpdBuilderTest); @@ -110,13 +118,11 @@ TEST_F(OnDemandMpdBuilderTest, Video) { } TEST_F(OnDemandMpdBuilderTest, TwoVideosWithDifferentResolutions) { - AdaptationSet* adaptation_set = mpd_.AddAdaptationSet(""); - MediaInfo media_info1 = GetTestMediaInfo(kFileNameVideoMediaInfo1); - ASSERT_TRUE(adaptation_set->AddRepresentation(media_info1)); - MediaInfo media_info2 = GetTestMediaInfo(kFileNameVideoMediaInfo2); - ASSERT_TRUE(adaptation_set->AddRepresentation(media_info2)); + // The order matters here to check against expected output. + ASSERT_NO_FATAL_FAILURE(AddRepresentation(media_info1)); + ASSERT_NO_FATAL_FAILURE(AddRepresentation(media_info2)); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputVideo1And2)); } @@ -125,21 +131,9 @@ TEST_F(OnDemandMpdBuilderTest, TwoVideosWithDifferentResolutions) { TEST_F(OnDemandMpdBuilderTest, VideoAndAudio) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); MediaInfo audio_media_info = GetTestMediaInfo(kFileNameAudioMediaInfo1); - // The order matters here to check against expected output. - AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(""); - ASSERT_TRUE(video_adaptation_set); - - AdaptationSet* audio_adaptation_set = mpd_.AddAdaptationSet(""); - ASSERT_TRUE(audio_adaptation_set); - - Representation* audio_representation = - audio_adaptation_set->AddRepresentation(audio_media_info); - ASSERT_TRUE(audio_representation); - - Representation* video_representation = - video_adaptation_set->AddRepresentation(video_media_info); - ASSERT_TRUE(video_representation); + ASSERT_NO_FATAL_FAILURE(AddRepresentation(video_media_info)); + ASSERT_NO_FATAL_FAILURE(AddRepresentation(audio_media_info)); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputAudio1AndVideo1)); } @@ -161,24 +155,23 @@ TEST_F(OnDemandMpdBuilderTest, MediaInfoMissingBandwidth) { TEST_F(LiveMpdBuilderTest, DynamicCheckMpdAttributes) { static const char kExpectedOutput[] = "\n" - "\n" - "\n" - " \n" - "\n"; + "\n" + "\n"; std::string mpd_doc; mutable_mpd_options()->mpd_type = MpdType::kDynamic; + mutable_mpd_options()->mpd_params.minimum_update_period = 2; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); ASSERT_EQ(kExpectedOutput, mpd_doc); } @@ -186,20 +179,17 @@ TEST_F(LiveMpdBuilderTest, DynamicCheckMpdAttributes) { TEST_F(LiveMpdBuilderTest, StaticCheckMpdAttributes) { static const char kExpectedOutput[] = "\n" - "\n" - "\n" - " \n" - "\n"; + "\n" + "\n"; std::string mpd_doc; mutable_mpd_options()->mpd_type = MpdType::kStatic; diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc new file mode 100644 index 0000000000..f9cf2801c2 --- /dev/null +++ b/packager/mpd/base/period.cc @@ -0,0 +1,237 @@ +// Copyright 2017 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/period.h" + +#include "packager/base/stl_util.h" +#include "packager/mpd/base/adaptation_set.h" +#include "packager/mpd/base/mpd_options.h" +#include "packager/mpd/base/mpd_utils.h" +#include "packager/mpd/base/xml/xml_node.h" + +namespace shaka { +namespace { + +// 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) { + return content_protection1.SerializeAsString() == + content_protection2.SerializeAsString(); +} + +std::set GetUUIDs( + const MediaInfo::ProtectedContent& protected_content) { + std::set uuids; + for (const auto& entry : protected_content.content_protection_entry()) + uuids.insert(entry.uuid()); + return uuids; +} + +} // namespace + +Period::Period(const MpdOptions& mpd_options, + base::AtomicSequenceNumber* adaptation_set_counter, + base::AtomicSequenceNumber* representation_counter) + : mpd_options_(mpd_options), + adaptation_set_counter_(adaptation_set_counter), + representation_counter_(representation_counter) {} + +AdaptationSet* Period::GetOrCreateAdaptationSet( + const MediaInfo& media_info, + bool content_protection_in_adaptation_set) { + // AdaptationSets with the same key should only differ in ContentProtection, + // which also means that if |content_protection_in_adaptation_set| is false, + // there should be at most one entry in |adaptation_sets|. + const std::string key = GetAdaptationSetKey(media_info); + std::list& adaptation_sets = adaptation_set_list_map_[key]; + if (content_protection_in_adaptation_set) { + for (AdaptationSet* adaptation_set : adaptation_sets) { + if (protected_adaptation_set_map_.Match(*adaptation_set, media_info)) + return adaptation_set; + } + } else { + if (!adaptation_sets.empty()) { + DCHECK_EQ(adaptation_sets.size(), 1u); + return adaptation_sets.front(); + } + } + // None of the adaptation sets match with the new content protection. + // Need a new one. + std::string language = GetLanguage(media_info); + std::unique_ptr new_adaptation_set = + NewAdaptationSet(adaptation_set_counter_->GetNext(), language, + mpd_options_, representation_counter_); + if (!SetNewAdaptationSetAttributes(language, media_info, adaptation_sets, + new_adaptation_set.get())) { + return nullptr; + } + + if (content_protection_in_adaptation_set && + media_info.has_protected_content()) { + protected_adaptation_set_map_.Register(*new_adaptation_set, media_info); + AddContentProtectionElements(media_info, new_adaptation_set.get()); + + for (AdaptationSet* adaptation_set : adaptation_sets) { + if (protected_adaptation_set_map_.Switchable(*adaptation_set, + *new_adaptation_set)) { + new_adaptation_set->AddAdaptationSetSwitching(adaptation_set->id()); + adaptation_set->AddAdaptationSetSwitching(new_adaptation_set->id()); + } + } + } + adaptation_sets.push_back(new_adaptation_set.get()); + adaptation_sets_.push_back(std::move(new_adaptation_set)); + return adaptation_sets_.back().get(); +} + +xml::scoped_xml_ptr Period::GetXml() { + xml::XmlNode period("Period"); + + // Always set id=0 for now. + // Required for 'dynamic' MPDs. + period.SetId(0); + // Iterate thru AdaptationSets and add them to one big Period element. + for (const auto& adaptation_set : adaptation_sets_) { + xml::scoped_xml_ptr child(adaptation_set->GetXml()); + if (!child || !period.AddChild(std::move(child))) + return nullptr; + } + + // TODO(kqyang): Should we set @start unconditionally to 0? + if (mpd_options_.mpd_type == MpdType::kDynamic) { + // This is the only Period and it is a regular period. + period.SetStringAttribute("start", "PT0S"); + } + return period.PassScopedPtr(); +} + +bool Period::GetEarliestTimestamp(double* timestamp_seconds) { + DCHECK(timestamp_seconds); + + double earliest_timestamp(-1); + for (const std::unique_ptr& adaptation_set : + adaptation_sets_) { + double timestamp; + if (adaptation_set->GetEarliestTimestamp(×tamp) && + (earliest_timestamp < 0 || timestamp < earliest_timestamp)) { + DCHECK_GE(timestamp, 0); + earliest_timestamp = timestamp; + } + } + if (earliest_timestamp < 0) + return false; + + *timestamp_seconds = earliest_timestamp; + return true; +} + +std::unique_ptr Period::NewAdaptationSet( + uint32_t adaptation_set_id, + const std::string& language, + const MpdOptions& options, + base::AtomicSequenceNumber* representation_counter) { + return std::unique_ptr(new AdaptationSet( + adaptation_set_id, language, options, representation_counter)); +} + +bool Period::SetNewAdaptationSetAttributes( + const std::string& language, + const MediaInfo& media_info, + const std::list& adaptation_sets, + AdaptationSet* new_adaptation_set) { + if (!language.empty() && language == mpd_options_.mpd_params.default_language) + new_adaptation_set->AddRole(AdaptationSet::kRoleMain); + + if (media_info.has_video_info()) { + // Because 'language' is ignored for videos, |adaptation_sets| must have + // all the video AdaptationSets. + if (adaptation_sets.size() > 1) { + new_adaptation_set->AddRole(AdaptationSet::kRoleMain); + } else if (adaptation_sets.size() == 1) { + (*adaptation_sets.begin())->AddRole(AdaptationSet::kRoleMain); + new_adaptation_set->AddRole(AdaptationSet::kRoleMain); + } + + if (media_info.video_info().has_playback_rate()) { + uint32_t trick_play_reference_id = 0; + if (!FindOriginalAdaptationSetForTrickPlay(media_info, + &trick_play_reference_id)) { + LOG(ERROR) << "Failed to find main adaptation set for trick play."; + return false; + } + DCHECK_NE(new_adaptation_set->id(), trick_play_reference_id); + new_adaptation_set->AddTrickPlayReferenceId(trick_play_reference_id); + } + } else if (media_info.has_text_info()) { + // IOP requires all AdaptationSets to have (sub)segmentAlignment set to + // true, so carelessly set it to true. + // In practice it doesn't really make sense to adapt between text tracks. + new_adaptation_set->ForceSetSegmentAlignment(true); + } + return true; +} + +bool Period::FindOriginalAdaptationSetForTrickPlay( + const MediaInfo& media_info, + uint32_t* main_adaptation_set_id) { + MediaInfo media_info_no_trickplay = media_info; + media_info_no_trickplay.mutable_video_info()->clear_playback_rate(); + + std::string key = GetAdaptationSetKey(media_info_no_trickplay); + const std::list& adaptation_sets = + adaptation_set_list_map_[key]; + for (AdaptationSet* adaptation_set : adaptation_sets) { + if (protected_adaptation_set_map_.Match(*adaptation_set, media_info)) { + *main_adaptation_set_id = adaptation_set->id(); + return true; + } + } + return false; +} + +void Period::ProtectedAdaptationSetMap::Register( + const AdaptationSet& adaptation_set, + const MediaInfo& media_info) { + DCHECK(!ContainsKey(protected_content_map_, adaptation_set.id())); + protected_content_map_[adaptation_set.id()] = media_info.protected_content(); +} + +bool Period::ProtectedAdaptationSetMap::Match( + const AdaptationSet& adaptation_set, + const MediaInfo& media_info) { + const auto protected_content_it = + protected_content_map_.find(adaptation_set.id()); + // If the AdaptationSet ID is not registered in the map, then it is clear + // content. + if (protected_content_it == protected_content_map_.end()) + return !media_info.has_protected_content(); + if (!media_info.has_protected_content()) + return false; + return ProtectedContentEq(protected_content_it->second, + media_info.protected_content()); +} + +bool Period::ProtectedAdaptationSetMap::Switchable( + const AdaptationSet& adaptation_set_a, + const AdaptationSet& adaptation_set_b) { + const auto protected_content_it_a = + protected_content_map_.find(adaptation_set_a.id()); + const auto protected_content_it_b = + protected_content_map_.find(adaptation_set_b.id()); + + if (protected_content_it_a == protected_content_map_.end()) + return protected_content_it_b == protected_content_map_.end(); + if (protected_content_it_b == protected_content_map_.end()) + return false; + // Get all the UUIDs of the AdaptationSet. If another AdaptationSet has the + // same UUIDs then those are switchable. + return GetUUIDs(protected_content_it_a->second) == + GetUUIDs(protected_content_it_b->second); +} + +} // namespace shaka diff --git a/packager/mpd/base/period.h b/packager/mpd/base/period.h new file mode 100644 index 0000000000..6634267978 --- /dev/null +++ b/packager/mpd/base/period.h @@ -0,0 +1,135 @@ +// Copyright 2017 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 +// +/// All the methods that are virtual are virtual for mocking. + +#ifndef PACKAGER_MPD_BASE_PERIOD_H_ +#define PACKAGER_MPD_BASE_PERIOD_H_ + +#include +#include + +#include "packager/base/atomic_sequence_num.h" +#include "packager/mpd/base/adaptation_set.h" +#include "packager/mpd/base/media_info.pb.h" +#include "packager/mpd/base/xml/scoped_xml_ptr.h" + +namespace shaka { + +struct MpdOptions; + +namespace xml { +class XmlNode; +} // namespace xml + +/// Period class maps to element and provides methods to add +/// AdaptationSets. +class Period { + public: + /// Check the existing AdaptationSets, if there is one matching the provided + /// @a media_info, return it; otherwise a new AdaptationSet is created and + /// returned. + /// @param media_info contains media information, which is used to match + /// AdaptationSets. + /// @param content_protection_in_adaptation_set determines if the + /// ContentProtection is placed in AdaptationSet or Representation + /// element. This affects how MediaInfo in AdaptationSets are matched. + /// @return the AdaptationSet matching @a media_info if found; otherwise + /// return a new AdaptationSet. + virtual AdaptationSet* GetOrCreateAdaptationSet( + const MediaInfo& media_info, + bool content_protection_in_adaptation_set); + + /// Generates xml element with its child AdaptationSet elements. + /// @return On success returns a non-NULL scoped_xml_ptr. Otherwise returns a + /// NULL scoped_xml_ptr. + xml::scoped_xml_ptr GetXml(); + + protected: + /// @param mpd_options is the options for this MPD. + /// @param adaptation_set_counter is a counter for assigning ID numbers to + /// AdaptationSet. It can not be NULL. + /// @param representation_counter is a counter for assigning ID numbers to + /// Representation. It can not be NULL. + Period(const MpdOptions& mpd_options, + base::AtomicSequenceNumber* adaptation_set_counter, + base::AtomicSequenceNumber* representation_counter); + + private: + Period(const Period&) = delete; + Period& operator=(const Period&) = delete; + + friend class MpdBuilder; + friend class PeriodTest; + + // Gets the earliest, normalized segment timestamp. Returns true on success, + // false otherwise. + bool GetEarliestTimestamp(double* timestamp_seconds); + + // Calls AdaptationSet constructor. For mock injection. + virtual std::unique_ptr NewAdaptationSet( + uint32_t adaptation_set_id, + const std::string& lang, + const MpdOptions& options, + base::AtomicSequenceNumber* representation_counter); + + // Helper function to set new AdaptationSet attributes. + bool SetNewAdaptationSetAttributes( + const std::string& language, + const MediaInfo& media_info, + const std::list& adaptation_sets, + AdaptationSet* new_adaptation_set); + + // Gets the original AdaptationSet which the trick play video belongs + // to and returns the id of the original adapatation set. + // It is assumed that the corresponding AdaptationSet has been created before + // the trick play AdaptationSet. + // Returns true if main_adaptation_id is found, otherwise false; + bool FindOriginalAdaptationSetForTrickPlay( + const MediaInfo& media_info, + uint32_t* original_adaptation_set_id); + + const MpdOptions& mpd_options_; + base::AtomicSequenceNumber* const adaptation_set_counter_; + base::AtomicSequenceNumber* const representation_counter_; + // The list of AdaptationSets in this Period. + std::list> adaptation_sets_; + // AdaptationSets grouped by a specific adaptation set grouping key. + // AdaptationSets with the same key contain identical parameters except + // ContentProtection parameters. A single AdaptationSet would be created + // if they contain identical ContentProtection elements. This map is only + // useful when ContentProtection element is placed in AdaptationSet. + std::map> adaptation_set_list_map_; + + // Tracks ProtectedContent in AdaptationSet. + class ProtectedAdaptationSetMap { + public: + ProtectedAdaptationSetMap() = default; + // Register the |adaptation_set| with associated |media_info| in the map. + void Register(const AdaptationSet& adaptation_set, + const MediaInfo& media_info); + // Check if the protected content associated with |adaptation_set| matches + // with the one in |media_info|. + bool Match(const AdaptationSet& adaptation_set, + const MediaInfo& media_info); + // Check if the two adaptation sets are switchable. + bool Switchable(const AdaptationSet& adaptation_set_a, + const AdaptationSet& adaptation_set_b); + + private: + ProtectedAdaptationSetMap(const ProtectedAdaptationSetMap&) = delete; + ProtectedAdaptationSetMap& operator=(const ProtectedAdaptationSetMap&) = + delete; + + // AdaptationSet id => ProtectedContent map. + std::map protected_content_map_; + }; + ProtectedAdaptationSetMap protected_adaptation_set_map_; +}; + +} // namespace shaka + +#endif // PACKAGER_MPD_BASE_PERIOD_H_ diff --git a/packager/mpd/base/period_unittest.cc b/packager/mpd/base/period_unittest.cc new file mode 100644 index 0000000000..93e1d85d25 --- /dev/null +++ b/packager/mpd/base/period_unittest.cc @@ -0,0 +1,773 @@ +// Copyright 2017 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/period.h" + +#include +#include + +#include "packager/mpd/base/mock_mpd_builder.h" +#include "packager/mpd/base/mpd_options.h" +#include "packager/mpd/test/mpd_builder_test_helper.h" +#include "packager/mpd/test/xml_compare.h" + +using ::testing::_; +using ::testing::ByMove; +using ::testing::ElementsAre; +using ::testing::Eq; +using ::testing::InSequence; +using ::testing::Return; +using ::testing::StrictMock; +using ::testing::UnorderedElementsAre; + +namespace shaka { +namespace { +const uint32_t kDefaultAdaptationSetId = 0u; +const uint32_t kTrickPlayAdaptationSetId = 1u; +const bool kContentProtectionInAdaptationSet = true; + +bool ElementEqual(const Element& lhs, const 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 ContentProtectionElement& lhs, + const 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); +} + +/// A Period class that is capable of injecting mocked AdaptationSet. +class TestablePeriod : public Period { + public: + TestablePeriod(const MpdOptions& mpd_options) + : Period(mpd_options, &sequence_number_, &sequence_number_) {} + + MOCK_METHOD4(NewAdaptationSet, + std::unique_ptr( + uint32_t adaptation_set_id, + const std::string& lang, + const MpdOptions& options, + base::AtomicSequenceNumber* representation_counter)); + + private: + // Only for constructing the super class. Not used for testing. + base::AtomicSequenceNumber sequence_number_; +}; + +} // namespace + +class PeriodTest : public ::testing::Test { + public: + PeriodTest() + : testable_period_(mpd_options_), + default_adaptation_set_( + new StrictMock(kDefaultAdaptationSetId)), + default_adaptation_set_ptr_(default_adaptation_set_.get()) {} + + protected: + MpdOptions mpd_options_; + TestablePeriod testable_period_; + + // Default mock that can be used for the tests. + std::unique_ptr> default_adaptation_set_; + StrictMock* default_adaptation_set_ptr_; +}; + +TEST_F(PeriodTest, GetXml) { + const char kVideoMediaInfo[] = + "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"; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVideoMediaInfo), + kContentProtectionInAdaptationSet)); + + const char kExpectedXml[] = + "" + // ContentType and Representation elements are populated after + // Representation::Init() is called. + " " + ""; + EXPECT_THAT(testable_period_.GetXml().get(), XmlNodeEqual(kExpectedXml)); +} + +TEST_F(PeriodTest, DynamicMpdGetXml) { + const char kVideoMediaInfo[] = + "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"; + mpd_options_.mpd_type = MpdType::kDynamic; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVideoMediaInfo), + kContentProtectionInAdaptationSet)); + + const char kExpectedXml[] = + "" + // ContentType and Representation elements are populated after + // Representation::Init() is called. + " " + ""; + EXPECT_THAT(testable_period_.GetXml().get(), XmlNodeEqual(kExpectedXml)); +} + +// Verify ForceSetSegmentAlignment is called. +TEST_F(PeriodTest, Text) { + const char kTextMediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + " language: 'en'\n" + "}\n" + "container_type: CONTAINER_TEXT\n"; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, Eq("en"), _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))); + EXPECT_CALL(*default_adaptation_set_ptr_, ForceSetSegmentAlignment(true)); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kTextMediaInfo), + kContentProtectionInAdaptationSet)); +} + +// Verify AddTrickPlayReferenceId is called. +TEST_F(PeriodTest, TrickPlayWithMatchingAdaptationSet) { + const char kVideoMediaInfo[] = + "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 char kTrickPlayMediaInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + " playback_rate: 10\n" + "}\n" + "container_type: 1\n"; + + std::unique_ptr> trick_play_adaptation_set( + new StrictMock(kTrickPlayAdaptationSetId)); + auto* trick_play_adaptation_set_ptr = trick_play_adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))) + .WillOnce(Return(ByMove(std::move(trick_play_adaptation_set)))); + + EXPECT_CALL(*trick_play_adaptation_set_ptr, + AddTrickPlayReferenceId(Eq(kDefaultAdaptationSetId))); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVideoMediaInfo), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(trick_play_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kTrickPlayMediaInfo), + kContentProtectionInAdaptationSet)); +} + +// Verify no AdaptationSet is returned on trickplay media info. +TEST_F(PeriodTest, TrickPlayWithNoMatchingAdaptationSet) { + const char kVideoMediaInfo[] = + "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 char kVp9TrickPlayMediaInfo[] = + "video_info {\n" + " codec: 'vp9'\n" + " width: 1280\n" + " height: 720\n" + " time_scale: 10\n" + " frame_duration: 100\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + " playback_rate: 10\n" + "}\n" + "container_type: 1\n"; + + std::unique_ptr> trick_play_adaptation_set( + new StrictMock(kTrickPlayAdaptationSetId)); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))) + .WillOnce(Return(ByMove(std::move(trick_play_adaptation_set)))); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVideoMediaInfo), + kContentProtectionInAdaptationSet)); + // A nullptr is returned if it is not able to find matching AdaptationSet. + ASSERT_FALSE(testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVp9TrickPlayMediaInfo), + kContentProtectionInAdaptationSet)); +} + +// Verify with different MediaInfo::ProtectedContent, two AdaptationSets should +// be created. AdaptationSets with different DRM won't be switchable. +TEST_F(PeriodTest, DifferentProtectedContent) { + // 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; + std::unique_ptr> sd_adaptation_set( + new StrictMock(kSdAdaptationSetId)); + auto* sd_adaptation_set_ptr = sd_adaptation_set.get(); + std::unique_ptr> hd_adaptation_set( + new StrictMock(kHdAdaptationSetId)); + auto* hd_adaptation_set_ptr = hd_adaptation_set.get(); + + InSequence in_sequence; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(sd_adaptation_set)))); + + EXPECT_CALL( + *sd_adaptation_set_ptr, + AddContentProtectionElement(ContentProtectionElementEq(mp4_protection))); + EXPECT_CALL( + *sd_adaptation_set_ptr, + AddContentProtectionElement(ContentProtectionElementEq(sd_my_drm))); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(hd_adaptation_set)))); + + // Add main Role here for both. + EXPECT_CALL(*sd_adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + EXPECT_CALL(*hd_adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + + // Called twice for the same reason as above. + EXPECT_CALL(*hd_adaptation_set_ptr, AddContentProtectionElement(_)).Times(2); + + ASSERT_EQ(sd_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kSdProtectedContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(hd_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kHdProtectedContent), + kContentProtectionInAdaptationSet)); + + EXPECT_THAT(sd_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre()); + EXPECT_THAT(hd_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre()); +} + +// Verify with the same MediaInfo::ProtectedContent, only one AdaptationSets +// should be created. +TEST_F(PeriodTest, SameProtectedContent) { + // 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); + + InSequence in_sequence; + + // Only called once. + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(default_adaptation_set_)))); + + EXPECT_CALL( + *default_adaptation_set_ptr_, + AddContentProtectionElement(ContentProtectionElementEq(mp4_protection))); + EXPECT_CALL(*default_adaptation_set_ptr_, + AddContentProtectionElement(ContentProtectionElementEq(my_drm))); + + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kSdProtectedContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(default_adaptation_set_ptr_, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kHdProtectedContent), + kContentProtectionInAdaptationSet)); + + // No adaptation set switching if there is only one AdaptationSet. + EXPECT_THAT(default_adaptation_set_ptr_->adaptation_set_switching_ids(), + ElementsAre()); +} + +// Default Key IDs are different but if the content protection UUIDs match, then +// the AdaptationSet they belong to should be switchable. +// This is a long test. +// Basically this +// 1. Add an SD protected content. This should make an AdaptationSet. +// 2. Add an HD protected content. This should make another AdaptationSet that +// is different from the SD version. SD AdaptationSet and HD AdaptationSet +// should be switchable. +// 3. Add a 4k protected content. This should also make a new AdaptationSet. +// It should be switchable with SD/HD AdaptationSet. +TEST_F(PeriodTest, SetAdaptationSetSwitching) { + // 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; + std::unique_ptr> sd_adaptation_set( + new StrictMock(kSdAdaptationSetId)); + auto* sd_adaptation_set_ptr = sd_adaptation_set.get(); + std::unique_ptr> hd_adaptation_set( + new StrictMock(kHdAdaptationSetId)); + auto* hd_adaptation_set_ptr = hd_adaptation_set.get(); + + InSequence in_sequence; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(sd_adaptation_set)))); + + EXPECT_CALL(*sd_adaptation_set_ptr, AddContentProtectionElement(_)).Times(2); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(hd_adaptation_set)))); + + // Add main Role here for both. + EXPECT_CALL(*sd_adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + EXPECT_CALL(*hd_adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + + EXPECT_CALL(*hd_adaptation_set_ptr, AddContentProtectionElement(_)).Times(2); + + ASSERT_EQ(sd_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kSdProtectedContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(hd_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kHdProtectedContent), + kContentProtectionInAdaptationSet)); + + EXPECT_THAT(sd_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre(kHdAdaptationSetId)); + EXPECT_THAT(hd_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre(kSdAdaptationSetId)); + + // Add another content that has the same protected content and make sure that + // adaptation set switching is set correctly. + 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; + std::unique_ptr> fourk_adaptation_set( + new StrictMock(k4kAdaptationSetId)); + auto* fourk_adaptation_set_ptr = fourk_adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(fourk_adaptation_set)))); + + EXPECT_CALL(*fourk_adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + EXPECT_CALL(*fourk_adaptation_set_ptr, AddContentProtectionElement(_)) + .Times(2); + + ASSERT_EQ(fourk_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(k4kProtectedContent), + kContentProtectionInAdaptationSet)); + + EXPECT_THAT(sd_adaptation_set_ptr->adaptation_set_switching_ids(), + UnorderedElementsAre(kHdAdaptationSetId, k4kAdaptationSetId)); + EXPECT_THAT(hd_adaptation_set_ptr->adaptation_set_switching_ids(), + UnorderedElementsAre(kSdAdaptationSetId, k4kAdaptationSetId)); + EXPECT_THAT(fourk_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre(kSdAdaptationSetId, kHdAdaptationSetId)); +} + +// Even if the UUIDs match, video and audio AdaptationSets should not be +// switchable. +TEST_F(PeriodTest, DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) { + // 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 kVideoAdaptationSetId = 6u; + const uint32_t kAudioAdaptationSetId = 7u; + std::unique_ptr> video_adaptation_set( + new StrictMock(kVideoAdaptationSetId)); + auto* video_adaptation_set_ptr = video_adaptation_set.get(); + std::unique_ptr> audio_adaptation_set( + new StrictMock(kAudioAdaptationSetId)); + auto* audio_adaptation_set_ptr = audio_adaptation_set.get(); + + InSequence in_sequence; + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(video_adaptation_set)))); + EXPECT_CALL(*video_adaptation_set_ptr, AddContentProtectionElement(_)) + .Times(2); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(audio_adaptation_set)))); + EXPECT_CALL(*audio_adaptation_set_ptr, AddContentProtectionElement(_)) + .Times(2); + + ASSERT_EQ(video_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVideoContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(audio_adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAudioContent), + kContentProtectionInAdaptationSet)); + + EXPECT_THAT(video_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre()); + EXPECT_THAT(audio_adaptation_set_ptr->adaptation_set_switching_ids(), + ElementsAre()); +} + +// Don't put different audio languages or codecs in the same AdaptationSet. +TEST_F(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { + const char kAacEnglishAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'eng'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + + const char kAacGermanAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + + const char kVorbisGermanAudioContent1[] = + "audio_info {\n" + " codec: 'vorbis'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_WEBM\n" + "media_duration_seconds: 10.5\n"; + + const char kVorbisGermanAudioContent2[] = + "audio_info {\n" + " codec: 'vorbis'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_WEBM\n" + "media_duration_seconds: 10.5\n"; + + std::unique_ptr> aac_eng_adaptation_set( + new StrictMock(1)); + auto* aac_eng_adaptation_set_ptr = aac_eng_adaptation_set.get(); + std::unique_ptr> aac_ger_adaptation_set( + new StrictMock(2)); + auto* aac_ger_adaptation_set_ptr = aac_ger_adaptation_set.get(); + std::unique_ptr> vorbis_german_adaptation_set( + new StrictMock(3)); + auto* vorbis_german_adaptation_set_ptr = vorbis_german_adaptation_set.get(); + + // We expect three AdaptationSets. + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(aac_eng_adaptation_set)))) + .WillOnce(Return(ByMove(std::move(aac_ger_adaptation_set)))) + .WillOnce(Return(ByMove(std::move(vorbis_german_adaptation_set)))); + + ASSERT_EQ(aac_eng_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacEnglishAudioContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(aac_ger_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacGermanAudioContent), + kContentProtectionInAdaptationSet)); + ASSERT_EQ(vorbis_german_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVorbisGermanAudioContent1), + kContentProtectionInAdaptationSet)); + // The same AdaptationSet is returned. + ASSERT_EQ(vorbis_german_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVorbisGermanAudioContent2), + kContentProtectionInAdaptationSet)); +} + +} // namespace shaka diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index 3201a4be90..247a7887a7 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -7,6 +7,7 @@ #include "packager/mpd/base/representation.h" #include "packager/base/logging.h" +#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/mpd_utils.h" #include "packager/mpd/base/xml/xml_node.h" diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index 45ba671828..e80be0df68 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -6,9 +6,11 @@ // /// All the methods that are virtual are virtual for mocking. +#ifndef PACKAGER_MPD_BASE_REPRESENTATION_H_ +#define PACKAGER_MPD_BASE_REPRESENTATION_H_ + #include "packager/mpd/base/bandwidth_estimator.h" #include "packager/mpd/base/media_info.pb.h" -#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/segment_info.h" #include "packager/mpd/base/xml/scoped_xml_ptr.h" @@ -20,6 +22,7 @@ namespace shaka { struct ContentProtectionElement; +struct MpdOptions; namespace xml { class XmlNode; @@ -198,3 +201,5 @@ class Representation { }; } // namespace shaka + +#endif // PACKAGER_MPD_BASE_REPRESENTATION_H_ diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index e3551325ee..2017d1b21b 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -11,6 +11,7 @@ #include #include "packager/base/strings/stringprintf.h" +#include "packager/mpd/base/mpd_options.h" #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index e00610e198..de40d91344 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -12,8 +12,13 @@ #include "packager/mpd/base/mpd_builder.h" #include "packager/mpd/base/mpd_notifier_util.h" #include "packager/mpd/base/mpd_utils.h" +#include "packager/mpd/base/period.h" #include "packager/mpd/base/representation.h" +namespace { +const bool kContentProtectionInAdaptationSet = true; +} // namespace + namespace shaka { SimpleMpdNotifier::SimpleMpdNotifier(const MpdOptions& mpd_options) @@ -40,20 +45,16 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, return false; base::AutoLock auto_lock(lock_); + if (!period_) + period_ = mpd_builder_->AddPeriod(); + AdaptationSet* adaptation_set = period_->GetOrCreateAdaptationSet( + media_info, !kContentProtectionInAdaptationSet); + DCHECK(adaptation_set); - // TODO(kqyang): Consider adding a new method MpdBuilder::AddRepresentation. - // Most of the codes here can be moved inside. - std::string key = GetAdaptationSetKey(media_info); - std::string lang = GetLanguage(media_info); - AdaptationSet** adaptation_set = &adaptation_set_map_[key]; - if (*adaptation_set == NULL) - *adaptation_set = mpd_builder_->AddAdaptationSet(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); + adaptation_set->AddRepresentation(adjusted_media_info); if (representation == NULL) return false; diff --git a/packager/mpd/base/simple_mpd_notifier.h b/packager/mpd/base/simple_mpd_notifier.h index 1ceba2981a..906037342e 100644 --- a/packager/mpd/base/simple_mpd_notifier.h +++ b/packager/mpd/base/simple_mpd_notifier.h @@ -21,6 +21,7 @@ namespace shaka { class AdaptationSet; class MpdBuilder; +class Period; class Representation; class SimpleMpdNotifierTest; @@ -69,6 +70,7 @@ class SimpleMpdNotifier : public MpdNotifier { // MPD output path. std::string output_path_; std::unique_ptr mpd_builder_; + Period* period_ = nullptr; base::Lock lock_; typedef std::map AdaptationSetMap; diff --git a/packager/mpd/base/simple_mpd_notifier_unittest.cc b/packager/mpd/base/simple_mpd_notifier_unittest.cc index 7723398884..7268541cda 100644 --- a/packager/mpd/base/simple_mpd_notifier_unittest.cc +++ b/packager/mpd/base/simple_mpd_notifier_unittest.cc @@ -4,6 +4,8 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +#include +#include #include #include "packager/base/files/file_path.h" @@ -17,33 +19,48 @@ namespace shaka { using ::testing::_; +using ::testing::Eq; using ::testing::Return; using ::testing::StrEq; 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 bool kContentProtectionInAdaptationSet = true; + +MATCHER_P(EqualsProto, message, "") { + return ::google::protobuf::util::MessageDifferencer::Equals(arg, message); +} + } // namespace class SimpleMpdNotifierTest : public ::testing::Test { protected: SimpleMpdNotifierTest() - : default_mock_adaptation_set_( + : default_mock_period_(new MockPeriod), + default_mock_adaptation_set_( new MockAdaptationSet(kDefaultAdaptationSetId)) {} void SetUp() override { ASSERT_TRUE(base::CreateTemporaryFile(&temp_file_path_)); empty_mpd_option_.mpd_params.mpd_output = temp_file_path_.AsUTF8Unsafe(); + + // Three valid media info. The actual data does not matter. + 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"; + valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo); + valid_media_info2_ = valid_media_info1_; + valid_media_info2_.mutable_video_info()->set_width(960); + valid_media_info3_ = valid_media_info1_; + valid_media_info3_.mutable_video_info()->set_width(480); } void TearDown() override { @@ -55,14 +72,21 @@ class SimpleMpdNotifierTest : public ::testing::Test { notifier->SetMpdBuilderForTesting(std::move(mpd_builder)); } + protected: // Empty mpd options except with output path specified, so that // WriteMpdToFile() doesn't crash. MpdOptions empty_mpd_option_; const std::vector empty_base_urls_; - // Default AdaptationSet mock. + // Default mocks that can be used for the tests. + std::unique_ptr default_mock_period_; std::unique_ptr default_mock_adaptation_set_; + // Three valid media info. The actual content does not matter. + MediaInfo valid_media_info1_; + MediaInfo valid_media_info2_; + MediaInfo valid_media_info3_; + private: base::FilePath temp_file_path_; }; @@ -76,9 +100,14 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewContainer) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), + Eq(!kContentProtectionInAdaptationSet))) .WillOnce(Return(default_mock_adaptation_set_.get())); - EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + EXPECT_CALL(*default_mock_adaptation_set_, + AddRepresentation(EqualsProto(valid_media_info1_))) .WillOnce(Return(mock_representation.get())); // This is for the Flush() below but adding expectation here because the next @@ -87,8 +116,8 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewContainer) { uint32_t unused_container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info1_, &unused_container_id)); EXPECT_TRUE(notifier.Flush()); } @@ -100,15 +129,16 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(mock_representation.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); EXPECT_EQ(kRepresentationId, container_id); const uint32_t kSampleDuration = 100; @@ -125,8 +155,7 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) { TEST_F(SimpleMpdNotifierTest, NotifyNewContainerAndSampleDurationNoMock) { SimpleMpdNotifier notifier(empty_mpd_option_); uint32_t container_id; - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); const uint32_t kAnySampleDuration = 1000; EXPECT_TRUE(notifier.NotifySampleDuration(container_id, kAnySampleDuration)); EXPECT_TRUE(notifier.Flush()); @@ -140,15 +169,16 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(mock_representation.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); EXPECT_EQ(kRepresentationId, container_id); const uint64_t kStartTime = 0u; @@ -169,15 +199,16 @@ TEST_F(SimpleMpdNotifierTest, AddContentProtectionElement) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(mock_representation.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); EXPECT_EQ(kRepresentationId, container_id); ContentProtectionElement element; @@ -186,40 +217,22 @@ TEST_F(SimpleMpdNotifierTest, AddContentProtectionElement) { } TEST_F(SimpleMpdNotifierTest, UpdateEncryption) { - const char kProtectedContent[] = - "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: 'psshsomethingelse'\n" - " }\n" - " default_key_id: '_default_key_id_'\n" - "}\n" - "container_type: 1\n"; SimpleMpdNotifier notifier(empty_mpd_option_); const uint32_t kRepresentationId = 447834u; std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) .WillOnce(Return(mock_representation.get())); uint32_t container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kProtectedContent), - &container_id)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); ::testing::Mock::VerifyAndClearExpectations( default_mock_adaptation_set_.get()); @@ -239,66 +252,13 @@ TEST_F(SimpleMpdNotifierTest, UpdateEncryption) { container_id, "myuuid", std::vector(), kBogusNewPsshVector)); } -// Don't put different audio languages or codecs in the same AdaptationSet. -TEST_F(SimpleMpdNotifierTest, SplitAdaptationSetsByLanguageAndCodec) { - // MP4, English - const char kAudioContent1[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'eng'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - // MP4, German - const char kAudioContent2[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - // WebM, German - const char kAudioContent3[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - - // WebM, German again - const char kAudioContent4[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - +// Test multiple media info with some belongs to the same AdaptationSets. +TEST_F(SimpleMpdNotifierTest, MultipleMediaInfo) { SimpleMpdNotifier notifier(empty_mpd_option_); std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); std::unique_ptr adaptation_set1(new MockAdaptationSet(1)); std::unique_ptr adaptation_set2(new MockAdaptationSet(2)); - std::unique_ptr adaptation_set3(new MockAdaptationSet(3)); std::unique_ptr representation1( new MockRepresentation(1)); @@ -306,35 +266,40 @@ TEST_F(SimpleMpdNotifierTest, SplitAdaptationSetsByLanguageAndCodec) { new MockRepresentation(2)); std::unique_ptr representation3( new MockRepresentation(3)); - std::unique_ptr representation4( - new MockRepresentation(4)); - // We expect three AdaptationSets. - EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_)) - .WillOnce(Return(adaptation_set1.get())) - .WillOnce(Return(adaptation_set2.get())) - .WillOnce(Return(adaptation_set3.get())); - // The first AdaptationSet should have Eng MP4, one Representation. - EXPECT_CALL(*adaptation_set1, AddRepresentation(_)) + EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + .WillOnce(Return(default_mock_period_.get())); + + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) + .WillOnce(Return(adaptation_set1.get())); + EXPECT_CALL(*adaptation_set1, + AddRepresentation(EqualsProto(valid_media_info1_))) .WillOnce(Return(representation1.get())); - // The second AdaptationSet should have Ger MP4, one Representation. - EXPECT_CALL(*adaptation_set2, AddRepresentation(_)) + // Return the same adaptation set for |valid_media_info2_| and + // |valid_media_info3_|. This results in AddRepresentation to be called twice + // on |adaptation_set2|. + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info2_), _)) + .WillOnce(Return(adaptation_set2.get())); + EXPECT_CALL(*adaptation_set2, + AddRepresentation(EqualsProto(valid_media_info2_))) .WillOnce(Return(representation2.get())); - // The third AdaptationSet should have Ger WebM, two Representations. - EXPECT_CALL(*adaptation_set3, AddRepresentation(_)) - .WillOnce(Return(representation3.get())) - .WillOnce(Return(representation4.get())); + EXPECT_CALL(*default_mock_period_, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info3_), _)) + .WillOnce(Return(adaptation_set2.get())); + EXPECT_CALL(*adaptation_set2, + AddRepresentation(EqualsProto(valid_media_info3_))) + .WillOnce(Return(representation3.get())); uint32_t unused_container_id; SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent1), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent2), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent3), &unused_container_id)); - EXPECT_TRUE(notifier.NotifyNewContainer( - ConvertToMediaInfo(kAudioContent4), &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info1_, &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info2_, &unused_container_id)); + EXPECT_TRUE( + notifier.NotifyNewContainer(valid_media_info3_, &unused_container_id)); } } // namespace shaka diff --git a/packager/mpd/mpd.gyp b/packager/mpd/mpd.gyp index ef85a39617..9eb04ea12d 100644 --- a/packager/mpd/mpd.gyp +++ b/packager/mpd/mpd.gyp @@ -43,6 +43,8 @@ 'base/mpd_options.h', 'base/mpd_utils.cc', 'base/mpd_utils.h', + 'base/period.cc', + 'base/period.h', 'base/representation.cc', 'base/representation.h', 'base/segment_info.h', @@ -88,6 +90,7 @@ 'base/bandwidth_estimator_unittest.cc', 'base/dash_iop_mpd_notifier_unittest.cc', 'base/mpd_builder_unittest.cc', + 'base/period_unittest.cc', 'base/representation_unittest.cc', 'base/simple_mpd_notifier_unittest.cc', 'base/xml/xml_node_unittest.cc', diff --git a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt index 69e8ad6330..ce0e0fc1c6 100644 --- a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt @@ -2,7 +2,7 @@ - + test_output_file_name1.mp4 @@ -10,7 +10,7 @@ - + test_output_file_name_audio1.mp4