From ef2c424876c01c34217a5c09c3ffc823939f811e Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Thu, 4 Jan 2018 18:31:27 -0800 Subject: [PATCH] Implement Representation::GetDurationSeconds Also added Period::GetAdaptationSets and AdaptationSet::GetRepresentations. Instead of implementing GetDurationSeconds in all MpdBuilder classes (MpdBuilder/Period/Adaptation/Representation) like what we used to do with GetEarliestTimestamp, the two new functions allows MpdBuilder to iterate through the Representations to get durations. Also updates GetEarliestTimestamp functions to use the same iteration method. Change-Id: I682b70c07c248c0f6511ec3d9019086f986ee10e --- packager/mpd/base/adaptation_set.cc | 18 ++--- packager/mpd/base/adaptation_set.h | 7 +- packager/mpd/base/adaptation_set_unittest.cc | 46 ++++++++++-- packager/mpd/base/mpd_builder.cc | 75 ++++++++------------ packager/mpd/base/period.cc | 19 ++--- packager/mpd/base/period.h | 7 +- packager/mpd/base/period_unittest.cc | 54 +++++++++++++- packager/mpd/base/representation.cc | 35 ++++----- packager/mpd/base/representation.h | 13 ++-- packager/mpd/base/representation_unittest.cc | 26 +++++++ 10 files changed, 181 insertions(+), 119 deletions(-) diff --git a/packager/mpd/base/adaptation_set.cc b/packager/mpd/base/adaptation_set.cc index 3645d98483..5e6a737173 100644 --- a/packager/mpd/base/adaptation_set.cc +++ b/packager/mpd/base/adaptation_set.cc @@ -387,23 +387,13 @@ void AdaptationSet::AddTrickPlayReferenceId(uint32_t id) { trick_play_reference_ids_.insert(id); } -bool AdaptationSet::GetEarliestTimestamp(double* timestamp_seconds) { - DCHECK(timestamp_seconds); - - double earliest_timestamp(-1); +const std::list AdaptationSet::GetRepresentations() const { + std::list representations; for (const std::unique_ptr& representation : representations_) { - double timestamp; - if (representation->GetEarliestTimestamp(×tamp) && - ((earliest_timestamp < 0) || (timestamp < earliest_timestamp))) { - earliest_timestamp = timestamp; - } + representations.push_back(representation.get()); } - if (earliest_timestamp < 0) - return false; - - *timestamp_seconds = earliest_timestamp; - return true; + return representations; } // This implementation assumes that each representations' segments' are diff --git a/packager/mpd/base/adaptation_set.h b/packager/mpd/base/adaptation_set.h index 605d7cd942..821d5889b3 100644 --- a/packager/mpd/base/adaptation_set.h +++ b/packager/mpd/base/adaptation_set.h @@ -147,6 +147,9 @@ class AdaptationSet { /// @param id the id of the reference (or main) adapation set. virtual void AddTrickPlayReferenceId(uint32_t id); + // Return the list of Representations in this AdaptationSet. + const std::list GetRepresentations() const; + protected: /// @param adaptation_set_id is an ID number for this AdaptationSet. /// @param lang is the language of this AdaptationSet. Mainly relevant for @@ -188,10 +191,6 @@ class AdaptationSet { // 2 -> [0, 200, 400] typedef std::map> RepresentationTimeline; - // Gets the earliest, normalized segment timestamp. Returns true if - // successful, false otherwise. - bool GetEarliestTimestamp(double* timestamp_seconds); - /// Called from OnNewSegmentForRepresentation(). Checks whether the segments /// are aligned. Sets segments_aligned_. /// This is only for Live. For VOD, CheckVodSegmentAlignment() should be used. diff --git a/packager/mpd/base/adaptation_set_unittest.cc b/packager/mpd/base/adaptation_set_unittest.cc index 17224d1f80..7b6887e779 100644 --- a/packager/mpd/base/adaptation_set_unittest.cc +++ b/packager/mpd/base/adaptation_set_unittest.cc @@ -16,6 +16,7 @@ #include "packager/mpd/test/xml_compare.h" using ::testing::Not; +using ::testing::UnorderedElementsAre; namespace shaka { @@ -563,6 +564,43 @@ TEST_F(AdaptationSetTest, BubbleUpAttributesToAdaptationSet) { EXPECT_THAT(no_common_attributes.get(), Not(AttributeSet("frameRate"))); } +TEST_F(AdaptationSetTest, GetRepresentations) { + const char k480pMediaInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 8\n" + " pixel_height: 9\n" + "}\n" + "container_type: 1\n"; + const char k360pMediaInfo[] = + "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" + "container_type: 1\n"; + + auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, kNoLanguage); + + Representation* representation_480p = + adaptation_set->AddRepresentation(ConvertToMediaInfo(k480pMediaInfo)); + EXPECT_THAT(adaptation_set->GetRepresentations(), + UnorderedElementsAre(representation_480p)); + + Representation* representation_360p = + adaptation_set->AddRepresentation(ConvertToMediaInfo(k360pMediaInfo)); + EXPECT_THAT(adaptation_set->GetRepresentations(), + UnorderedElementsAre(representation_360p, representation_480p)); +} + // Verify that subsegmentAlignment is set to true if all the Representations' // segments are aligned and the DASH profile is OnDemand. // Also checking that not all Representations have to be added before calling @@ -961,9 +999,7 @@ TEST_F(OnDemandAdaptationSetTest, const char kExpectedOutput[] = "" " " + " mimeType=\"audio/mp4\" audioSamplingRate=\"44100\">" " \n" " " + " mimeType=\"application/ttml+xml\">" " subtitle.xml" " " ""; diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index 3f1a623ec4..f71e2e971c 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -16,6 +16,7 @@ #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/mpd_utils.h" #include "packager/mpd/base/period.h" +#include "packager/mpd/base/representation.h" #include "packager/mpd/base/xml/xml_node.h" #include "packager/version/version.h" @@ -44,28 +45,6 @@ void AddMpdNameSpaceInfo(XmlNode* mpd) { mpd->SetStringAttribute("xmlns:cenc", kCencNamespace); } -bool IsPeriodNode(xmlNodePtr node) { - DCHECK(node); - int kEqual = 0; - return xmlStrcmp(node->name, reinterpret_cast("Period")) == - kEqual; -} - -// Find the first element. This does not recurse down the tree, -// only checks direct children. Returns the pointer to Period element on -// success, otherwise returns false. -// As noted here, we must traverse. -// http://www.xmlsoft.org/tutorial/ar01s04.html -xmlNodePtr FindPeriodNode(XmlNode* xml_node) { - for (xmlNodePtr node = xml_node->GetRawPtr()->xmlChildrenNode; node != NULL; - node = node->next) { - if (IsPeriodNode(node)) - return node; - } - - return NULL; -} - bool Positive(double d) { return d > 0.0; } @@ -306,44 +285,46 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { DCHECK(mpd_node); DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type); + if (periods_.empty()) { + LOG(WARNING) << "No Period found. Set MPD duration to 0."; + return 0.0f; + } + // 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); - representation; - representation = xmlNextElementSibling(representation)) { - float duration = 0.0f; - if (GetDurationAttribute(representation, &duration)) { - max_duration = max_duration > duration ? max_duration : duration; - - // 'duration' attribute is there only to help generate MPD, not - // necessary for MPD, remove the attribute. - xmlUnsetProp(representation, BAD_CAST "duration"); - } + // TODO(kqyang): Right now all periods contain the duration for the whole MPD. + // Simply get the duration from the first period. Ideally the period duration + // should only count the (sub)segments in that period. + for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) { + for (const auto* representation : adaptation_set->GetRepresentations()) { + max_duration = + std::max(representation->GetDurationSeconds(), max_duration); } } - return max_duration; } bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) { DCHECK(timestamp_seconds); DCHECK(!periods_.empty()); - return periods_.front()->GetEarliestTimestamp(timestamp_seconds); + double timestamp = 0; + double earliest_timestamp = -1; + // The first period should have the earliest timestamp. + for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) { + for (const auto* representation : adaptation_set->GetRepresentations()) { + if (representation->GetEarliestTimestamp(×tamp) && + (earliest_timestamp < 0 || timestamp < earliest_timestamp)) { + earliest_timestamp = timestamp; + } + } + } + if (earliest_timestamp < 0) + return false; + *timestamp_seconds = earliest_timestamp; + return true; } void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path, diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc index 9cbdeb4ada..e62c76dcc5 100644 --- a/packager/mpd/base/period.cc +++ b/packager/mpd/base/period.cc @@ -110,24 +110,13 @@ xml::scoped_xml_ptr Period::GetXml() { return period.PassScopedPtr(); } -bool Period::GetEarliestTimestamp(double* timestamp_seconds) { - DCHECK(timestamp_seconds); - - double earliest_timestamp(-1); +const std::list Period::GetAdaptationSets() const { + std::list adaptation_sets; 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; - } + adaptation_sets.push_back(adaptation_set.get()); } - if (earliest_timestamp < 0) - return false; - - *timestamp_seconds = earliest_timestamp; - return true; + return adaptation_sets; } std::unique_ptr Period::NewAdaptationSet( diff --git a/packager/mpd/base/period.h b/packager/mpd/base/period.h index 340460a53d..a352de03e5 100644 --- a/packager/mpd/base/period.h +++ b/packager/mpd/base/period.h @@ -50,6 +50,9 @@ class Period { /// NULL scoped_xml_ptr. xml::scoped_xml_ptr GetXml(); + /// @return The list of AdaptationSets in this Period. + const std::list GetAdaptationSets() const; + protected: /// @param mpd_options is the options for this MPD. /// @param adaptation_set_counter is a counter for assigning ID numbers to @@ -67,10 +70,6 @@ class Period { 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, diff --git a/packager/mpd/base/period_unittest.cc b/packager/mpd/base/period_unittest.cc index f1a97b88e0..ac6031e44e 100644 --- a/packager/mpd/base/period_unittest.cc +++ b/packager/mpd/base/period_unittest.cc @@ -712,7 +712,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { "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" @@ -724,7 +723,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { "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" @@ -736,7 +734,6 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { "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" @@ -784,6 +781,57 @@ TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { content_protection_in_adaptation_set_)); } +TEST_P(PeriodTest, GetAdaptationSets) { + 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"; + + 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(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _, _)) + .WillOnce(Return(ByMove(std::move(aac_eng_adaptation_set)))) + .WillOnce(Return(ByMove(std::move(aac_ger_adaptation_set)))); + + ASSERT_EQ(aac_eng_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacEnglishAudioContent), + content_protection_in_adaptation_set_)); + EXPECT_THAT(testable_period_.GetAdaptationSets(), + UnorderedElementsAre(aac_eng_adaptation_set_ptr)); + + ASSERT_EQ(aac_ger_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacGermanAudioContent), + content_protection_in_adaptation_set_)); + EXPECT_THAT(testable_period_.GetAdaptationSets(), + UnorderedElementsAre(aac_eng_adaptation_set_ptr, + aac_ger_adaptation_set_ptr)); +} + INSTANTIATE_TEST_CASE_P(ContentProtectionInAdaptationSet, PeriodTest, ::testing::Bool()); diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index 247a7887a7..ddc351dc22 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -255,15 +255,6 @@ xml::scoped_xml_ptr Representation::GetXml() { return xml::scoped_xml_ptr(); } - // Set media duration for static mpd. - if (mpd_options_.mpd_type == MpdType::kStatic && - media_info_.has_media_duration_seconds()) { - // Adding 'duration' attribute, so that this information can be used when - // generating one MPD file. This should be removed from the final MPD. - representation.SetFloatingPointAttribute( - "duration", media_info_.media_duration_seconds()); - } - if (HasVODOnlyFields(media_info_) && !representation.AddVODOnlyInfo(media_info_)) { LOG(ERROR) << "Failed to add VOD segment info."; @@ -287,6 +278,21 @@ void Representation::SuppressOnce(SuppressFlag flag) { output_suppression_flags_ |= flag; } +bool Representation::GetEarliestTimestamp(double* timestamp_seconds) const { + DCHECK(timestamp_seconds); + + if (segment_infos_.empty()) + return false; + + *timestamp_seconds = static_cast(segment_infos_.begin()->start_time) / + GetTimeScale(media_info_); + return true; +} + +float Representation::GetDurationSeconds() const { + return media_info_.media_duration_seconds(); +} + bool Representation::HasRequiredMediaInfoFields() { if (HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)) { LOG(ERROR) << "MediaInfo cannot have both VOD and Live fields."; @@ -453,15 +459,4 @@ std::string Representation::GetTextMimeType() const { return ""; } -bool Representation::GetEarliestTimestamp(double* timestamp_seconds) { - DCHECK(timestamp_seconds); - - if (segment_infos_.empty()) - return false; - - *timestamp_seconds = static_cast(segment_infos_.begin()->start_time) / - GetTimeScale(media_info_); - return true; -} - } // namespace shaka diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index e80be0df68..9925f0cbdd 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -125,6 +125,13 @@ class Representation { /// This may be called multiple times to set different (or the same) flags. void SuppressOnce(SuppressFlag flag); + /// Gets the earliest, normalized segment timestamp. + /// @return true if successful, false otherwise. + bool GetEarliestTimestamp(double* timestamp_seconds) const; + + /// @return The duration of the Representation in seconds. + float GetDurationSeconds() const; + /// @return ID number for . uint32_t id() const { return id_; } @@ -150,8 +157,6 @@ class Representation { friend class AdaptationSet; friend class RepresentationTest; - bool AddLiveInfo(xml::RepresentationXmlNode* representation); - // Returns true if |media_info_| has required fields to generate a valid // Representation. Otherwise returns false. bool HasRequiredMediaInfoFields(); @@ -172,10 +177,6 @@ class Representation { std::string GetAudioMimeType() const; std::string GetTextMimeType() const; - // Gets the earliest, normalized segment timestamp. Returns true if - // successful, false otherwise. - bool GetEarliestTimestamp(double* timestamp_seconds); - // Init() checks that only one of VideoInfo, AudioInfo, or TextInfo is set. So // any logic using this can assume only one set. MediaInfo media_info_; diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index 2017d1b21b..d72124642d 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -484,6 +484,32 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) { EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); } +TEST_F(SegmentTemplateTest, GetEarliestTimestamp) { + double earliest_timestamp; + // No segments. + EXPECT_FALSE(representation_->GetEarliestTimestamp(&earliest_timestamp)); + + const uint64_t kStartTime = 88; + const uint64_t kDuration = 10; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + AddSegments(kStartTime + kDuration, kDuration, kSize, 0); + ASSERT_TRUE(representation_->GetEarliestTimestamp(&earliest_timestamp)); + EXPECT_EQ(static_cast(kStartTime) / kDefaultTimeScale, + earliest_timestamp); +} + +TEST_F(SegmentTemplateTest, GetDuration) { + const float kMediaDurationSeconds = 88.8f; + MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); + media_info.set_media_duration_seconds(kMediaDurationSeconds); + representation_ = + CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); + ASSERT_TRUE(representation_->Init()); + + EXPECT_EQ(kMediaDurationSeconds, representation_->GetDurationSeconds()); +} + TEST_F(SegmentTemplateTest, NormalRepeatedSegmentDuration) { const uint64_t kSize = 256; uint64_t start_time = 0;