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;