From d76ccea46f59f2999037b7ce727f7178aced670f Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Tue, 2 Jan 2018 16:10:54 -0800 Subject: [PATCH] [DASH] Support multiple period Change-Id: Ifd17bf0eabbd61ec7a1d35f0b864b5aa6666aa87 --- packager/mpd/base/adaptation_set.cc | 77 +++++++++------- packager/mpd/base/adaptation_set.h | 21 ++++- packager/mpd/base/adaptation_set_unittest.cc | 25 ++++++ packager/mpd/base/media_info.proto | 1 + packager/mpd/base/mock_mpd_builder.cc | 8 +- packager/mpd/base/mock_mpd_builder.h | 8 +- packager/mpd/base/mpd_builder.cc | 15 +++- packager/mpd/base/mpd_builder.h | 11 ++- packager/mpd/base/mpd_builder_unittest.cc | 23 ++++- packager/mpd/base/period.cc | 19 ++-- packager/mpd/base/period.h | 11 ++- packager/mpd/base/period_unittest.cc | 12 ++- packager/mpd/base/representation.cc | 22 +++++ packager/mpd/base/representation.h | 14 +++ packager/mpd/base/representation_unittest.cc | 39 +++++++++ packager/mpd/base/simple_mpd_notifier.cc | 87 ++++++++++++++----- packager/mpd/base/simple_mpd_notifier.h | 15 +++- .../mpd/base/simple_mpd_notifier_unittest.cc | 76 ++++++++++++++-- packager/mpd/base/xml/xml_node.cc | 10 +++ packager/mpd/test/xml_compare.h | 4 +- 20 files changed, 409 insertions(+), 89 deletions(-) diff --git a/packager/mpd/base/adaptation_set.cc b/packager/mpd/base/adaptation_set.cc index 5e6a737173..5f8d067bb9 100644 --- a/packager/mpd/base/adaptation_set.cc +++ b/packager/mpd/base/adaptation_set.cc @@ -188,43 +188,30 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { // will die before AdaptationSet. std::unique_ptr listener( new RepresentationStateChangeListenerImpl(representation_id, this)); - std::unique_ptr representation(new Representation( + std::unique_ptr new_representation(new Representation( media_info, mpd_options_, representation_id, std::move(listener))); - if (!representation->Init()) { + if (!new_representation->Init()) { LOG(ERROR) << "Failed to initialize Representation."; return NULL; } + UpdateFromMediaInfo(media_info); + representations_.push_back(std::move(new_representation)); + return representations_.back().get(); +} - // For videos, record the width, height, and the frame rate to calculate the - // max {width,height,framerate} required for DASH IOP. - if (media_info.has_video_info()) { - const MediaInfo::VideoInfo& video_info = media_info.video_info(); - DCHECK(video_info.has_width()); - DCHECK(video_info.has_height()); - video_widths_.insert(video_info.width()); - video_heights_.insert(video_info.height()); +Representation* AdaptationSet::CopyRepresentationWithTimeOffset( + const Representation& representation, + uint64_t presentation_time_offset) { + // Note that AdaptationSet outlive Representation, so this object + // will die before AdaptationSet. + std::unique_ptr listener( + new RepresentationStateChangeListenerImpl(representation.id(), this)); + std::unique_ptr new_representation(new Representation( + representation, presentation_time_offset, std::move(listener))); - if (video_info.has_time_scale() && video_info.has_frame_duration()) - RecordFrameRate(video_info.frame_duration(), video_info.time_scale()); - - AddPictureAspectRatio(video_info, &picture_aspect_ratio_); - } - - if (media_info.has_video_info()) { - content_type_ = "video"; - } else if (media_info.has_audio_info()) { - content_type_ = "audio"; - } else if (media_info.has_text_info()) { - content_type_ = "text"; - - if (media_info.text_info().has_type() && - (media_info.text_info().type() != MediaInfo::TextInfo::UNKNOWN)) { - roles_.insert(MediaInfoTextTypeToRole(media_info.text_info().type())); - } - } - - representations_.push_back(std::move(representation)); + UpdateFromMediaInfo(new_representation->GetMediaInfo()); + representations_.push_back(std::move(new_representation)); return representations_.back().get(); } @@ -396,6 +383,36 @@ const std::list AdaptationSet::GetRepresentations() const { return representations; } +void AdaptationSet::UpdateFromMediaInfo(const MediaInfo& media_info) { + // For videos, record the width, height, and the frame rate to calculate the + // max {width,height,framerate} required for DASH IOP. + if (media_info.has_video_info()) { + const MediaInfo::VideoInfo& video_info = media_info.video_info(); + DCHECK(video_info.has_width()); + DCHECK(video_info.has_height()); + video_widths_.insert(video_info.width()); + video_heights_.insert(video_info.height()); + + if (video_info.has_time_scale() && video_info.has_frame_duration()) + RecordFrameRate(video_info.frame_duration(), video_info.time_scale()); + + AddPictureAspectRatio(video_info, &picture_aspect_ratio_); + } + + if (media_info.has_video_info()) { + content_type_ = "video"; + } else if (media_info.has_audio_info()) { + content_type_ = "audio"; + } else if (media_info.has_text_info()) { + content_type_ = "text"; + + if (media_info.text_info().has_type() && + (media_info.text_info().type() != MediaInfo::TextInfo::UNKNOWN)) { + roles_.insert(MediaInfoTextTypeToRole(media_info.text_info().type())); + } + } +} + // This implementation assumes that each representations' segments' are // contiguous. // Also assumes that all Representations are added before this is called. diff --git a/packager/mpd/base/adaptation_set.h b/packager/mpd/base/adaptation_set.h index 821d5889b3..d177e9a90a 100644 --- a/packager/mpd/base/adaptation_set.h +++ b/packager/mpd/base/adaptation_set.h @@ -59,6 +59,18 @@ class AdaptationSet { /// NULL. The returned pointer is owned by the AdaptationSet instance. virtual Representation* AddRepresentation(const MediaInfo& media_info); + /// Copy a Representation instance from @a representation in another + /// AdaptationSet. One use case is to duplicate Representation in different + /// periods. + /// @param representation is an existing Representation to be cloned from. + /// @param presentation_time_offset is the presentation time offset for the + /// new Representation instance. + /// @return On success, returns a pointer to Representation. Otherwise returns + /// NULL. The returned pointer is owned by the AdaptationSet instance. + virtual Representation* CopyRepresentationWithTimeOffset( + const Representation& representation, + uint64_t presentation_time_offset); + /// Add a ContenProtection element to the adaptation set. /// AdaptationSet does not add elements /// automatically to itself even if @a media_info.protected_content is @@ -111,6 +123,10 @@ class AdaptationSet { // Must be unique in the Period. uint32_t id() const { return id_; } + /// Set AdaptationSet@id. + /// @param id is the new ID to be set. + void set_id(uint32_t id) { id_ = id; } + /// Notifies the AdaptationSet instance that a new (sub)segment was added to /// the Representation with @a representation_id. /// This must be called every time a (sub)segment is added to a @@ -191,6 +207,9 @@ class AdaptationSet { // 2 -> [0, 200, 400] typedef std::map> RepresentationTimeline; + // Update AdaptationSet attributes for new MediaInfo. + void UpdateFromMediaInfo(const MediaInfo& media_info); + /// Called from OnNewSegmentForRepresentation(). Checks whether the segments /// are aligned. Sets segments_aligned_. /// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used. @@ -214,7 +233,7 @@ class AdaptationSet { base::AtomicSequenceNumber* const representation_counter_; - const uint32_t id_; + uint32_t id_; const std::string lang_; const MpdOptions& mpd_options_; diff --git a/packager/mpd/base/adaptation_set_unittest.cc b/packager/mpd/base/adaptation_set_unittest.cc index 7b6887e779..1d8b59c102 100644 --- a/packager/mpd/base/adaptation_set_unittest.cc +++ b/packager/mpd/base/adaptation_set_unittest.cc @@ -120,6 +120,31 @@ TEST_F(AdaptationSetTest, CheckAdaptationSetTextContentType) { AttributeEqual("contentType", "text")); } +TEST_F(AdaptationSetTest, CopyRepresentationWithTimeOffset) { + 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: CONTAINER_MP4\n"; + + auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, kNoLanguage); + Representation* representation = + adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo)); + + const uint64_t kPresentationTimeOffset = 80; + Representation* new_representation = + adaptation_set->CopyRepresentationWithTimeOffset(*representation, + kPresentationTimeOffset); + EXPECT_EQ(kPresentationTimeOffset, + new_representation->GetMediaInfo().presentation_time_offset()); +} + // Verify that language passed to the constructor sets the @lang field is set. TEST_F(AdaptationSetTest, CheckLanguageAttributeSet) { auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "en"); diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 968278518c..b6c0a28982 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -133,6 +133,7 @@ message MediaInfo { // This is the reference time scale if there are multiple VideoInfo and/or // AudioInfo. optional uint32 reference_time_scale = 13; + optional uint64 presentation_time_offset = 16; optional ContainerType container_type = 14 [default = CONTAINER_UNKNOWN]; // VOD only. diff --git a/packager/mpd/base/mock_mpd_builder.cc b/packager/mpd/base/mock_mpd_builder.cc index 35f186dfb9..bd5c6fe7af 100644 --- a/packager/mpd/base/mock_mpd_builder.cc +++ b/packager/mpd/base/mock_mpd_builder.cc @@ -13,8 +13,12 @@ const MpdOptions kDefaultMpdOptions; MockMpdBuilder::MockMpdBuilder() : MpdBuilder(kDefaultMpdOptions) {} MockMpdBuilder::~MockMpdBuilder() {} -MockPeriod::MockPeriod() - : Period(kDefaultMpdOptions, &sequence_counter_, &sequence_counter_) {} +MockPeriod::MockPeriod(uint32_t period_id, double start_time_in_seconds) + : Period(period_id, + start_time_in_seconds, + kDefaultMpdOptions, + &sequence_counter_, + &sequence_counter_) {} MockAdaptationSet::MockAdaptationSet(uint32_t adaptation_set_id) : AdaptationSet(adaptation_set_id, diff --git a/packager/mpd/base/mock_mpd_builder.h b/packager/mpd/base/mock_mpd_builder.h index 56c81a463d..e8ffa91620 100644 --- a/packager/mpd/base/mock_mpd_builder.h +++ b/packager/mpd/base/mock_mpd_builder.h @@ -24,13 +24,13 @@ class MockMpdBuilder : public MpdBuilder { MockMpdBuilder(); ~MockMpdBuilder() override; - MOCK_METHOD0(AddPeriod, Period*()); + MOCK_METHOD1(GetOrCreatePeriod, Period*(double start_time_in_seconds)); MOCK_METHOD1(ToString, bool(std::string* output)); }; class MockPeriod : public Period { public: - MockPeriod(); + MockPeriod(uint32_t period_id, double start_time_in_seconds); MOCK_METHOD2(GetOrCreateAdaptationSet, AdaptationSet*(const MediaInfo& media_info, @@ -48,6 +48,9 @@ class MockAdaptationSet : public AdaptationSet { ~MockAdaptationSet() override; MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info)); + MOCK_METHOD2(CopyRepresentationWithTimeOffset, + Representation*(const Representation& representation, + uint64_t presentation_time_offset)); MOCK_METHOD1(AddContentProtectionElement, void(const ContentProtectionElement& element)); MOCK_METHOD2(UpdateContentProtectionPssh, @@ -75,6 +78,7 @@ class MockRepresentation : public Representation { MOCK_METHOD3(AddNewSegment, void(uint64_t start_time, uint64_t duration, uint64_t size)); MOCK_METHOD1(SetSampleDuration, void(uint32_t sample_duration)); + MOCK_CONST_METHOD0(GetMediaInfo, const MediaInfo&()); }; } // namespace shaka diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index f71e2e971c..c7d2dc9947 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -119,9 +119,18 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) { base_urls_.push_back(base_url); } -Period* MpdBuilder::AddPeriod() { - periods_.emplace_back(new Period(mpd_options_, &adaptation_set_counter_, - &representation_counter_)); +Period* MpdBuilder::GetOrCreatePeriod(double start_time_in_seconds) { + for (auto& period : periods_) { + const double kPeriodTimeDriftThresholdInSeconds = 1.0; + const bool match = + std::fabs(period->start_time_in_seconds() - start_time_in_seconds) < + kPeriodTimeDriftThresholdInSeconds; + if (match) + return period.get(); + } + periods_.emplace_back( + new Period(period_counter_.GetNext(), start_time_in_seconds, mpd_options_, + &adaptation_set_counter_, &representation_counter_)); return periods_.back().get(); } diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index 5cb1b23ac1..1cda19e9c6 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -46,9 +46,13 @@ class MpdBuilder { /// @param base_url URL for entry. void AddBaseUrl(const std::string& base_url); - /// Adds to the MPD. - /// @return The new period, which is owned by this instance. - virtual Period* AddPeriod(); + /// Check the existing Periods, if there is one matching the provided + /// @a start_time_in_seconds, return it; otherwise a new Period is created and + /// returned. + /// @param start_time_in_seconds is the period start time. + /// @return the Period matching @a start_time_in_seconds if found; otherwise + /// return a new Period. + virtual Period* GetOrCreatePeriod(double start_time_in_seconds); /// Writes the MPD to the given string. /// @param[out] output is an output string where the MPD gets written. @@ -112,6 +116,7 @@ class MpdBuilder { std::list base_urls_; std::string availability_start_time_; + base::AtomicSequenceNumber period_counter_; base::AtomicSequenceNumber adaptation_set_counter_; base::AtomicSequenceNumber representation_counter_; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index a9a46ea3e1..f4309a8c32 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -37,7 +37,8 @@ class MpdBuilderTest : public ::testing::Test { ~MpdBuilderTest() override {} void SetUp() override { - period_ = mpd_.AddPeriod(); + const double kPeriodStartTimeSeconds = 0.0; + period_ = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds); ASSERT_TRUE(period_); } @@ -149,6 +150,26 @@ TEST_F(OnDemandMpdBuilderTest, MediaInfoMissingBandwidth) { ASSERT_FALSE(mpd_.ToString(&mpd_doc)); } +TEST_F(LiveMpdBuilderTest, MultiplePeriodTest) { + const double kPeriodStartTimeSeconds = 1.0; + Period* period = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds); + ASSERT_TRUE(period); + ASSERT_EQ(kPeriodStartTimeSeconds, period->start_time_in_seconds()); + + const double kPeriodStartTimeSeconds2 = 1.1; + Period* period2 = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds2); + ASSERT_TRUE(period2); + // The old Period is re-used if they are closed to each other. + ASSERT_EQ(period, period2); + ASSERT_EQ(kPeriodStartTimeSeconds, period2->start_time_in_seconds()); + + const double kPeriodStartTimeSeconds3 = 5.0; + Period* period3 = mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds3); + ASSERT_TRUE(period3); + ASSERT_NE(period, period3); + ASSERT_EQ(kPeriodStartTimeSeconds3, period3->start_time_in_seconds()); +} + // Check whether the attributes are set correctly for dynamic element. // This test must use ASSERT_EQ for comparison because XmlEqual() cannot // handle namespaces correctly yet. diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc index e62c76dcc5..21b48e1c9e 100644 --- a/packager/mpd/base/period.cc +++ b/packager/mpd/base/period.cc @@ -34,10 +34,14 @@ std::set GetUUIDs( } // namespace -Period::Period(const MpdOptions& mpd_options, +Period::Period(uint32_t period_id, + double start_time_in_seconds, + const MpdOptions& mpd_options, base::AtomicSequenceNumber* adaptation_set_counter, base::AtomicSequenceNumber* representation_counter) - : mpd_options_(mpd_options), + : id_(period_id), + start_time_in_seconds_(start_time_in_seconds), + mpd_options_(mpd_options), adaptation_set_counter_(adaptation_set_counter), representation_counter_(representation_counter) {} @@ -92,9 +96,8 @@ AdaptationSet* Period::GetOrCreateAdaptationSet( xml::scoped_xml_ptr Period::GetXml() { xml::XmlNode period("Period"); - // Always set id=0 for now. // Required for 'dynamic' MPDs. - period.SetId(0); + period.SetId(id_); // 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()); @@ -102,10 +105,10 @@ xml::scoped_xml_ptr Period::GetXml() { 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"); + if (mpd_options_.mpd_type == MpdType::kDynamic || + start_time_in_seconds_ != 0) { + period.SetStringAttribute("start", + SecondsToXmlDuration(start_time_in_seconds_)); } return period.PassScopedPtr(); } diff --git a/packager/mpd/base/period.h b/packager/mpd/base/period.h index a352de03e5..7a745c476d 100644 --- a/packager/mpd/base/period.h +++ b/packager/mpd/base/period.h @@ -53,13 +53,20 @@ class Period { /// @return The list of AdaptationSets in this Period. const std::list GetAdaptationSets() const; + /// @return The start time of this Period. + double start_time_in_seconds() const { return start_time_in_seconds_; } + protected: + /// @param period_id is an ID number for this Period. + /// @param start_time_in_seconds is the start time for this Period. /// @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, + Period(uint32_t period_id, + double start_time_in_seconds, + const MpdOptions& mpd_options, base::AtomicSequenceNumber* adaptation_set_counter, base::AtomicSequenceNumber* representation_counter); @@ -93,6 +100,8 @@ class Period { const MediaInfo& media_info, uint32_t* original_adaptation_set_id); + const uint32_t id_; + const double start_time_in_seconds_; const MpdOptions& mpd_options_; base::AtomicSequenceNumber* const adaptation_set_counter_; base::AtomicSequenceNumber* const representation_counter_; diff --git a/packager/mpd/base/period_unittest.cc b/packager/mpd/base/period_unittest.cc index ac6031e44e..b37289a384 100644 --- a/packager/mpd/base/period_unittest.cc +++ b/packager/mpd/base/period_unittest.cc @@ -25,6 +25,8 @@ using ::testing::UnorderedElementsAre; namespace shaka { namespace { +const uint32_t kDefaultPeriodId = 9u; +const double kDefaultPeriodStartTime = 5.6; const uint32_t kDefaultAdaptationSetId = 0u; const uint32_t kTrickPlayAdaptationSetId = 1u; @@ -75,7 +77,11 @@ MATCHER_P(ContentProtectionElementEq, expected, "") { class TestablePeriod : public Period { public: TestablePeriod(const MpdOptions& mpd_options) - : Period(mpd_options, &sequence_number_, &sequence_number_) {} + : Period(kDefaultPeriodId, + kDefaultPeriodStartTime, + mpd_options, + &sequence_number_, + &sequence_number_) {} MOCK_METHOD4(NewAdaptationSet, std::unique_ptr( @@ -133,7 +139,7 @@ TEST_P(PeriodTest, GetXml) { content_protection_in_adaptation_set_)); const char kExpectedXml[] = - "" + "" // ContentType and Representation elements are populated after // Representation::Init() is called. " " @@ -164,7 +170,7 @@ TEST_P(PeriodTest, DynamicMpdGetXml) { content_protection_in_adaptation_set_)); const char kExpectedXml[] = - "" + "" // ContentType and Representation elements are populated after // Representation::Init() is called. " " diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index ddc351dc22..0d5957a9a6 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -115,6 +115,24 @@ Representation::Representation( state_change_listener_(std::move(state_change_listener)), output_suppression_flags_(0) {} +Representation::Representation( + const Representation& representation, + uint64_t presentation_time_offset, + std::unique_ptr state_change_listener) + : Representation(representation.media_info_, + representation.mpd_options_, + representation.id_, + std::move(state_change_listener)) { + mime_type_ = representation.mime_type_; + codecs_ = representation.codecs_; + + start_number_ = representation.start_number_; + for (const SegmentInfo& segment_info : representation.segment_infos_) + start_number_ += segment_info.repeat + 1; + + media_info_.set_presentation_time_offset(presentation_time_offset); +} + Representation::~Representation() {} bool Representation::Init() { @@ -205,6 +223,10 @@ void Representation::SetSampleDuration(uint32_t sample_duration) { } } +const MediaInfo& Representation::GetMediaInfo() const { + return media_info_; +} + // Uses info in |media_info_| and |content_protection_elements_| to create a // "Representation" node. // MPD schema has strict ordering. The following must be done in order. diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index 9925f0cbdd..81eddb309d 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -112,6 +112,9 @@ class Representation { /// @param sample_duration is the duration of a sample. virtual void SetSampleDuration(uint32_t sample_duration); + /// @return MediaInfo for the Representation. + virtual const MediaInfo& GetMediaInfo() const; + /// @return Copy of . xml::scoped_xml_ptr GetXml(); @@ -150,6 +153,16 @@ class Representation { uint32_t representation_id, std::unique_ptr state_change_listener); + /// @param representation points to the original Representation to be cloned. + /// @param presentation_time_offset is the presentation time offset for the + /// new Representation. + /// @param state_change_listener is an event handler for state changes to + /// the representation. If null, no event handler registered. + Representation( + const Representation& representation, + uint64_t presentation_time_offset, + std::unique_ptr state_change_listener); + private: Representation(const Representation&) = delete; Representation& operator=(const Representation&) = delete; @@ -181,6 +194,7 @@ class Representation { // any logic using this can assume only one set. MediaInfo media_info_; std::list content_protection_elements_; + // TODO(kqyang): Address sliding window issue with multiple periods. std::list segment_infos_; const uint32_t id_; diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index d72124642d..537a40f327 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -52,6 +52,16 @@ class RepresentationTest : public ::testing::Test { std::move(state_change_listener))); } + std::unique_ptr CopyRepresentation( + const Representation& representation, + uint64_t presentation_time_offset, + std::unique_ptr + state_change_listener) { + return std::unique_ptr( + new Representation(representation, presentation_time_offset, + std::move(state_change_listener))); + } + std::unique_ptr NoListener() { return std::unique_ptr(); } @@ -484,6 +494,35 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) { EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); } +TEST_F(SegmentTemplateTest, RepresentationClone) { + MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); + media_info.set_segment_template("$Number$.mp4"); + representation_ = + CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); + ASSERT_TRUE(representation_->Init()); + + const uint64_t kStartTime = 0; + const uint64_t kDuration = 10; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + + const uint64_t kPresentationTimeOffset = 100; + auto cloned_representation = CopyRepresentation( + *representation_, kPresentationTimeOffset, NoListener()); + const char kExpectedXml[] = + "\n" + " \n" + " \n" + " \n" + "\n"; + EXPECT_THAT(cloned_representation->GetXml().get(), + XmlNodeEqual(kExpectedXml)); +} + TEST_F(SegmentTemplateTest, GetEarliestTimestamp) { double earliest_timestamp; // No segments. diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index e9946f6d4d..9faa0bb73e 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -41,32 +41,17 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, if (content_type == kContentTypeUnknown) return false; - base::AutoLock auto_lock(lock_); - if (!period_) - period_ = mpd_builder_->AddPeriod(); - AdaptationSet* adaptation_set = period_->GetOrCreateAdaptationSet( - media_info, content_protection_in_adaptation_set_); - DCHECK(adaptation_set); - MediaInfo adjusted_media_info(media_info); MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info); - Representation* representation = - adaptation_set->AddRepresentation(adjusted_media_info); + const Representation* kNoOriginalRepresentation = nullptr; + const double kPeriodStartTimeSeconds = 0.0; + + base::AutoLock auto_lock(lock_); + const Representation* representation = AddRepresentationToPeriod( + adjusted_media_info, kNoOriginalRepresentation, kPeriodStartTimeSeconds); if (!representation) return false; - - if (content_protection_in_adaptation_set_) { - // ContentProtection elements are already added to AdaptationSet above. - // Use RepresentationId to AdaptationSet map to update ContentProtection - // in AdaptationSet in NotifyEncryptionUpdate. - representation_id_to_adaptation_set_[representation->id()] = adaptation_set; - } else { - AddContentProtectionElements(media_info, representation); - } - *container_id = representation->id(); - DCHECK(!ContainsKey(representation_map_, representation->id())); - representation_map_[representation->id()] = representation; return true; } @@ -98,8 +83,30 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32_t container_id, bool SimpleMpdNotifier::NotifyCueEvent(uint32_t container_id, uint64_t timestamp) { - NOTIMPLEMENTED(); - return false; + base::AutoLock auto_lock(lock_); + auto it = representation_map_.find(container_id); + if (it == representation_map_.end()) { + LOG(ERROR) << "Unexpected container_id: " << container_id; + return false; + } + Representation* original_representation = it->second; + AdaptationSet* original_adaptation_set = + representation_id_to_adaptation_set_[container_id]; + + const MediaInfo& media_info = original_representation->GetMediaInfo(); + const double period_start_time_seconds = + static_cast(timestamp) / media_info.reference_time_scale(); + const Representation* new_representation = AddRepresentationToPeriod( + media_info, original_representation, period_start_time_seconds); + if (!new_representation) + return false; + + // TODO(kqyang): Pass the ID to GetOrCreateAdaptationSet instead? + AdaptationSet* new_adaptation_set = + representation_id_to_adaptation_set_[container_id]; + DCHECK(new_adaptation_set); + new_adaptation_set->set_id(original_adaptation_set->id()); + return true; } bool SimpleMpdNotifier::NotifyEncryptionUpdate( @@ -131,4 +138,38 @@ bool SimpleMpdNotifier::Flush() { return WriteMpdToFile(output_path_, mpd_builder_.get()); } +Representation* SimpleMpdNotifier::AddRepresentationToPeriod( + const MediaInfo& media_info, + const Representation* original_representation, + double period_start_time_seconds) { + Period* period = mpd_builder_->GetOrCreatePeriod(period_start_time_seconds); + DCHECK(period); + + AdaptationSet* adaptation_set = period->GetOrCreateAdaptationSet( + media_info, content_protection_in_adaptation_set_); + DCHECK(adaptation_set); + + Representation* representation = nullptr; + if (original_representation) { + representation = adaptation_set->CopyRepresentationWithTimeOffset( + *original_representation, + period->start_time_in_seconds() * media_info.reference_time_scale()); + } else { + representation = adaptation_set->AddRepresentation(media_info); + } + if (!representation) + return nullptr; + + if (content_protection_in_adaptation_set_) { + // ContentProtection elements are already added to AdaptationSet above. + // Use RepresentationId to AdaptationSet map to update ContentProtection + // in AdaptationSet in NotifyEncryptionUpdate. + representation_id_to_adaptation_set_[representation->id()] = adaptation_set; + } else { + AddContentProtectionElements(media_info, representation); + } + representation_map_[representation->id()] = representation; + return representation; +} + } // namespace shaka diff --git a/packager/mpd/base/simple_mpd_notifier.h b/packager/mpd/base/simple_mpd_notifier.h index 0863ebd580..a4b39ef285 100644 --- a/packager/mpd/base/simple_mpd_notifier.h +++ b/packager/mpd/base/simple_mpd_notifier.h @@ -20,7 +20,6 @@ namespace shaka { class AdaptationSet; class MpdBuilder; -class Period; class Representation; struct MpdOptions; @@ -39,7 +38,7 @@ class SimpleMpdNotifier : public MpdNotifier { bool NotifyNewContainer(const MediaInfo& media_info, uint32_t* id) override; bool NotifySampleDuration(uint32_t container_id, uint32_t sample_duration) override; - bool NotifyNewSegment(uint32_t id, + bool NotifyNewSegment(uint32_t container_id, uint64_t start_time, uint64_t duration, uint64_t size) override; @@ -57,6 +56,17 @@ class SimpleMpdNotifier : public MpdNotifier { friend class SimpleMpdNotifierTest; + // Add a new representation. If |original_representation| is not nullptr, the + // new Representation will clone from it; otherwise the new Representation is + // created from |media_info|. + // The new Representation will be added to Period with the specified start + // time. + // Returns the new Representation on success; otherwise a nullptr is returned. + Representation* AddRepresentationToPeriod( + const MediaInfo& media_info, + const Representation* original_representation, + double period_start_time_seconds); + // Testing only method. Returns a pointer to MpdBuilder. MpdBuilder* MpdBuilderForTesting() const { return mpd_builder_.get(); } @@ -68,7 +78,6 @@ class SimpleMpdNotifier : public MpdNotifier { // MPD output path. std::string output_path_; std::unique_ptr mpd_builder_; - Period* period_ = nullptr; // owned by |mpd_builder_|. bool content_protection_in_adaptation_set_ = true; base::Lock lock_; diff --git a/packager/mpd/base/simple_mpd_notifier_unittest.cc b/packager/mpd/base/simple_mpd_notifier_unittest.cc index a746e2735e..8469b7c1c4 100644 --- a/packager/mpd/base/simple_mpd_notifier_unittest.cc +++ b/packager/mpd/base/simple_mpd_notifier_unittest.cc @@ -20,11 +20,16 @@ namespace shaka { using ::testing::_; using ::testing::Eq; +using ::testing::Ref; using ::testing::Return; +using ::testing::ReturnRef; using ::testing::StrEq; namespace { +const uint32_t kDefaultPeriodId = 0u; +const double kDefaultPeriodStartTime = 0.0; const uint32_t kDefaultAdaptationSetId = 0u; +const uint32_t kDefaultTimeScale = 10; const bool kContentProtectionInAdaptationSet = true; MATCHER_P(EqualsProto, message, "") { @@ -36,7 +41,8 @@ MATCHER_P(EqualsProto, message, "") { class SimpleMpdNotifierTest : public ::testing::Test { protected: SimpleMpdNotifierTest() - : default_mock_period_(new MockPeriod), + : default_mock_period_( + new MockPeriod(kDefaultPeriodId, kDefaultPeriodStartTime)), default_mock_adaptation_set_( new MockAdaptationSet(kDefaultAdaptationSetId)) {} @@ -57,6 +63,7 @@ class SimpleMpdNotifierTest : public ::testing::Test { "}\n" "container_type: 1\n"; valid_media_info1_ = ConvertToMediaInfo(kValidMediaInfo); + valid_media_info1_.set_reference_time_scale(kDefaultTimeScale); valid_media_info2_ = valid_media_info1_; valid_media_info2_.mutable_video_info()->set_width(960); valid_media_info3_ = valid_media_info1_; @@ -100,7 +107,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewContainer) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) .WillOnce(Return(default_mock_period_.get())); EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) @@ -128,7 +135,7 @@ TEST_F(SimpleMpdNotifierTest, NotifySampleDuration) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) .WillOnce(Return(default_mock_period_.get())); EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); @@ -168,7 +175,7 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) { std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) .WillOnce(Return(default_mock_period_.get())); EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(_, _)) .WillOnce(Return(default_mock_adaptation_set_.get())); @@ -190,6 +197,58 @@ TEST_F(SimpleMpdNotifierTest, NotifyNewSegment) { kSegmentDuration, kSegmentSize)); } +TEST_F(SimpleMpdNotifierTest, NotifyCueEvent) { + SimpleMpdNotifier notifier(empty_mpd_option_); + + const uint32_t kRepresentationId = 123u; + std::unique_ptr mock_mpd_builder(new MockMpdBuilder()); + MockMpdBuilder* mock_mpd_builder_ptr = mock_mpd_builder.get(); + + std::unique_ptr mock_period( + new MockPeriod(kDefaultPeriodId, kDefaultPeriodStartTime)); + std::unique_ptr mock_adaptation_set( + new MockAdaptationSet(kDefaultAdaptationSetId)); + std::unique_ptr mock_representation( + new MockRepresentation(kRepresentationId)); + + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(Eq(0.0))) + .WillOnce(Return(mock_period.get())); + EXPECT_CALL(*mock_period, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) + .WillOnce(Return(mock_adaptation_set.get())); + EXPECT_CALL(*mock_adaptation_set, AddRepresentation(_)) + .WillOnce(Return(mock_representation.get())); + + uint32_t container_id; + SetMpdBuilder(¬ifier, std::move(mock_mpd_builder)); + EXPECT_TRUE(notifier.NotifyNewContainer(valid_media_info1_, &container_id)); + EXPECT_EQ(kRepresentationId, container_id); + + const uint32_t kAnotherPeriodId = 2u; + const double kArbitraryPeriodStartTime = 100; // Value does not matter. + std::unique_ptr mock_period2( + new MockPeriod(kAnotherPeriodId, kArbitraryPeriodStartTime)); + std::unique_ptr mock_adaptation_set2( + new MockAdaptationSet(kDefaultAdaptationSetId)); + std::unique_ptr mock_representation2( + new MockRepresentation(kRepresentationId)); + + const uint64_t kCueEventTimestamp = 1000; + EXPECT_CALL(*mock_representation, GetMediaInfo()) + .WillOnce(ReturnRef(valid_media_info1_)); + EXPECT_CALL(*mock_mpd_builder_ptr, + GetOrCreatePeriod(Eq(kCueEventTimestamp / kDefaultTimeScale))) + .WillOnce(Return(mock_period2.get())); + EXPECT_CALL(*mock_period2, + GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) + .WillOnce(Return(mock_adaptation_set2.get())); + EXPECT_CALL(*mock_adaptation_set2, + CopyRepresentationWithTimeOffset(Ref(*mock_representation), + kCueEventTimestamp)) + .WillOnce(Return(mock_representation2.get())); + EXPECT_TRUE(notifier.NotifyCueEvent(container_id, kCueEventTimestamp)); +} + TEST_F(SimpleMpdNotifierTest, ContentProtectionInAdaptationSetUpdateEncryption) { MpdOptions mpd_options = empty_mpd_option_; @@ -202,7 +261,7 @@ TEST_F(SimpleMpdNotifierTest, std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) .WillOnce(Return(default_mock_period_.get())); EXPECT_CALL( *default_mock_period_, @@ -245,7 +304,7 @@ TEST_F(SimpleMpdNotifierTest, std::unique_ptr mock_representation( new MockRepresentation(kRepresentationId)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) .WillOnce(Return(default_mock_period_.get())); EXPECT_CALL( *default_mock_period_, @@ -291,8 +350,9 @@ TEST_F(SimpleMpdNotifierTest, MultipleMediaInfo) { std::unique_ptr representation3( new MockRepresentation(3)); - EXPECT_CALL(*mock_mpd_builder, AddPeriod()) - .WillOnce(Return(default_mock_period_.get())); + EXPECT_CALL(*mock_mpd_builder, GetOrCreatePeriod(_)) + .Times(3) + .WillRepeatedly(Return(default_mock_period_.get())); EXPECT_CALL(*default_mock_period_, GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index 8309e33b96..2148c743e7 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -300,6 +300,11 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) { media_info.reference_time_scale()); } + if (media_info.has_presentation_time_offset()) { + segment_base.SetIntegerAttribute("presentationTimeOffset", + media_info.presentation_time_offset()); + } + if (media_info.has_init_range()) { XmlNode initialization("Initialization"); initialization.SetStringAttribute("range", @@ -326,6 +331,11 @@ bool RepresentationXmlNode::AddLiveOnlyInfo( media_info.reference_time_scale()); } + if (media_info.has_presentation_time_offset()) { + segment_template.SetIntegerAttribute("presentationTimeOffset", + media_info.presentation_time_offset()); + } + if (media_info.has_init_segment_name()) { // The spec does not allow '$Number$' and '$Time$' in initialization // attribute. diff --git a/packager/mpd/test/xml_compare.h b/packager/mpd/test/xml_compare.h index 721c2480bc..1759a81ac4 100644 --- a/packager/mpd/test/xml_compare.h +++ b/packager/mpd/test/xml_compare.h @@ -28,7 +28,9 @@ bool XmlEqual(const std::string& xml1, xmlNodePtr xml2); std::string XmlNodeToString(xmlNodePtr xml_node); /// Match an xmlNodePtr with an xml in string representation. -MATCHER_P(XmlNodeEqual, xml, "") { +MATCHER_P(XmlNodeEqual, + xml, + std::string("xml node equal (ignore extra white spaces)\n") + xml) { *result_listener << "\n" << XmlNodeToString(arg); return XmlEqual(xml, arg); }