From 4a0193a816c348c8534d9981958d18da10ffb6fa Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Wed, 21 May 2014 19:16:17 -0700 Subject: [PATCH] MpdBuilder Live profile Add BandwidthEstimator to estimate the required bandwidth for the segments. Also includes unit test for the change. Change-Id: I28262424e2ed6ceebdf81e2b11dcd67feba1d68c --- mpd/base/bandwidth_estimator.cc | 62 +++++ mpd/base/bandwidth_estimator.h | 42 ++++ mpd/base/bandwidth_estimator_test.cc | 94 ++++++++ mpd/base/mpd_builder.cc | 231 ++++++++++++++++-- mpd/base/mpd_builder.h | 55 ++++- mpd/base/mpd_builder_unittest.cc | 347 ++++++++++++++++++++++++--- mpd/base/mpd_utils.cc | 6 +- mpd/base/mpd_utils.h | 5 +- mpd/base/segment_info.h | 26 ++ mpd/base/simple_mpd_notifier.cc | 7 +- mpd/base/xml/xml_node.cc | 65 ++++- mpd/base/xml/xml_node.h | 6 + mpd/base/xml/xml_node_unittest.cc | 75 +++++- mpd/mpd.gyp | 3 + mpd/test/data/dynamic_normal_mpd.txt | 14 ++ mpd/test/mpd_builder_test_helper.h | 2 + mpd/util/mpd_writer.cc | 2 +- 17 files changed, 969 insertions(+), 73 deletions(-) create mode 100644 mpd/base/bandwidth_estimator.cc create mode 100644 mpd/base/bandwidth_estimator.h create mode 100644 mpd/base/bandwidth_estimator_test.cc create mode 100644 mpd/base/segment_info.h create mode 100644 mpd/test/data/dynamic_normal_mpd.txt diff --git a/mpd/base/bandwidth_estimator.cc b/mpd/base/bandwidth_estimator.cc new file mode 100644 index 0000000000..8fc413c133 --- /dev/null +++ b/mpd/base/bandwidth_estimator.cc @@ -0,0 +1,62 @@ +// Copyright 2014 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 "mpd/base/bandwidth_estimator.h" + +#include + +#include "base/logging.h" + +const int BandwidthEstimator::kUseAllBlocks = 0; + +BandwidthEstimator::BandwidthEstimator(int num_blocks) + : num_blocks_for_estimation_(num_blocks), + harmonic_mean_denominator_(0.0), + num_blocks_added_(0) {} +BandwidthEstimator::~BandwidthEstimator() {} + +void BandwidthEstimator::AddBlock(uint64 size, double duration) { + DCHECK_GT(duration, 0.0); + DCHECK_GT(size, 0u); + + if (num_blocks_for_estimation_ < 0 && + static_cast(history_.size()) >= -1 * num_blocks_for_estimation_) { + // Short circuiting the case where |num_blocks_for_estimation_| number of + // blocks have been added already. + return; + } + + const int kBitsInByte = 8; + const double bits_per_second_reciprocal = duration / (kBitsInByte * size); + harmonic_mean_denominator_ += bits_per_second_reciprocal; + if (num_blocks_for_estimation_ == kUseAllBlocks) { + DCHECK_EQ(history_.size(), 0u); + ++num_blocks_added_; + return; + } + + history_.push_back(bits_per_second_reciprocal); + if (num_blocks_for_estimation_ > 0 && + static_cast(history_.size()) > num_blocks_for_estimation_) { + harmonic_mean_denominator_ -= history_.front(); + history_.pop_front(); + } + + DCHECK_NE(num_blocks_for_estimation_, kUseAllBlocks); + DCHECK_LE(static_cast(history_.size()), abs(num_blocks_for_estimation_)); + DCHECK_EQ(num_blocks_added_, 0u); + return; +} + +uint64 BandwidthEstimator::Estimate() const { + if (harmonic_mean_denominator_ == 0.0) + return 0; + + const uint64 num_blocks = num_blocks_for_estimation_ == kUseAllBlocks + ? num_blocks_added_ + : history_.size(); + return static_cast(ceil(num_blocks / harmonic_mean_denominator_)); +} diff --git a/mpd/base/bandwidth_estimator.h b/mpd/base/bandwidth_estimator.h new file mode 100644 index 0000000000..254fe32454 --- /dev/null +++ b/mpd/base/bandwidth_estimator.h @@ -0,0 +1,42 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MPD_BASE_BANDWIDTH_ESTIMATOR_H_ +#define MPD_BASE_BANDWIDTH_ESTIMATOR_H_ + +#include + +#include "base/basictypes.h" + +class BandwidthEstimator { + public: + /// @param num_blocks is the number of latest blocks to use. Negative values + /// use first N blocks. 0 uses all. + explicit BandwidthEstimator(int num_blocks); + ~BandwidthEstimator(); + + // @param size is the size of the block in bytes. Should be positive. + // @param duration is the length in seconds. Should be positive. + void AddBlock(uint64 size, double duration); + + // @return The estimate bandwidth, in bits per second, from the harmonic mean + // of the number of blocks specified in the constructor. The value is + // rounded up to the nearest integer. + uint64 Estimate() const; + + static const int kUseAllBlocks; + + private: + const int num_blocks_for_estimation_; + double harmonic_mean_denominator_; + + // This is not used when num_blocks_for_estimation_ != 0. Therefore it should + // always be 0 if num_blocks_for_estimation_ != 0. + size_t num_blocks_added_; + std::list history_; +}; + +#endif // MPD_BASE_BANDWIDTH_ESTIMATOR_H_ diff --git a/mpd/base/bandwidth_estimator_test.cc b/mpd/base/bandwidth_estimator_test.cc new file mode 100644 index 0000000000..aea809ebfe --- /dev/null +++ b/mpd/base/bandwidth_estimator_test.cc @@ -0,0 +1,94 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include + +#include "mpd/base/bandwidth_estimator.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace dash_packager { + +namespace { +const int kNumBlocksForEstimate = 5; +const int kFirstOneBlockForEstimate = -1; +const uint64 kBitsInByte = 8; +const int kEstimateRoundError = 1; +} // namespace + +// Make sure that averaging of 5 blocks works, and also when there aren't all 5 +// blocks. +TEST(BandwidthEstimatorTest, FiveBlocksFiveBlocksAdded) { + BandwidthEstimator be(kNumBlocksForEstimate); + const double kDuration = 1.0; + const uint64 kExpectedResults[] = { + // Harmonic mean of [1 * 8], [1 * 8, 2 * 8], ... + // 8 is the number of bits in a byte and 1, 2, ... is from the loop + // counter below. + // Note that these are rounded up. + 8, + 11, + 14, + 16, + 18 + }; + + COMPILE_ASSERT(kNumBlocksForEstimate == arraysize(kExpectedResults), + incorrect_number_of_expectations); + for (uint64 i = 1; i <= arraysize(kExpectedResults); ++i) { + be.AddBlock(i, kDuration); + EXPECT_EQ(kExpectedResults[i - 1], be.Estimate()); + } +} + +// More practical situation where a lot of blocks get added but only the last 5 +// are considered for the estimate. +TEST(BandwidthEstimatorTest, FiveBlocksNormal) { + BandwidthEstimator be(kNumBlocksForEstimate); + const double kDuration = 10.0; + const uint64 kNumBlocksToAdd = 200; + const uint64 kExptectedEstimate = 800; + + // Doesn't matter what gets passed to the estimator except for the last 5 + // blocks which we add kExptectedEstimate / 8 bytes per second so that the + // estimate becomes kExptectedEstimate. + for (uint64 i = 1; i <= kNumBlocksToAdd; ++i) { + if (i > kNumBlocksToAdd - kNumBlocksForEstimate) { + be.AddBlock(kExptectedEstimate * kDuration / kBitsInByte, kDuration); + } else { + be.AddBlock(i, kDuration); + } + } + + EXPECT_NEAR(kExptectedEstimate, be.Estimate(), kEstimateRoundError); +} + +// Average all the blocks! +TEST(BandwidthEstimatorTest, AllBlocks) { + BandwidthEstimator be(BandwidthEstimator::kUseAllBlocks); + const uint64 kNumBlocksToAdd = 100; + const double kDuration = 1.0; + for (uint64 i = 1; i <= kNumBlocksToAdd; ++i) + be.AddBlock(i, kDuration); + + // The harmonic mean of 8, 16, ... , 800; rounded up. + const uint64 kExptectedEstimate = 155; + EXPECT_EQ(kExptectedEstimate, be.Estimate()); +} + +// Use only the first one. +TEST(BandwidthEstimatorTest, FirstOneBlock) { + BandwidthEstimator be(kFirstOneBlockForEstimate); + const double kDuration = 11.0; + const uint64 kExptectedEstimate = 123456; + be.AddBlock(kExptectedEstimate * kDuration / kBitsInByte, kDuration); + + // Anything. Should be ignored. + for (int i = 0; i < 1000; ++i) + be.AddBlock(100000, 10); + EXPECT_EQ(kExptectedEstimate, be.Estimate()); +} + +} // dash_packager diff --git a/mpd/base/mpd_builder.cc b/mpd/base/mpd_builder.cc index f5377b74ed..f32e174478 100644 --- a/mpd/base/mpd_builder.cc +++ b/mpd/base/mpd_builder.cc @@ -11,6 +11,8 @@ #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" +#include "base/time/time.h" #include "mpd/base/content_protection_element.h" #include "mpd/base/mpd_utils.h" #include "mpd/base/xml/xml_node.h" @@ -82,10 +84,47 @@ xmlNodePtr FindPeriodNode(XmlNode* xml_node) { return NULL; } +bool Positive(double d) { + return d > 0.0; +} + +// Return current time in XML DateTime format. +std::string XmlDateTimeNow() { + base::Time now = base::Time::Now(); + base::Time::Exploded now_exploded; + now.UTCExplode(&now_exploded); + + return base::StringPrintf("%4d-%02d-%02dT%02d:%02d:%02d", + now_exploded.year, + now_exploded.month, + now_exploded.day_of_month, + now_exploded.hour, + now_exploded.minute, + now_exploded.second); +} + +void SetIfPositive(const char* attr_name, double value, XmlNode* mpd) { + if (Positive(value)) { + mpd->SetStringAttribute(attr_name, SecondsToXmlDuration(value)); + } +} + } // namespace -MpdBuilder::MpdBuilder(MpdType type) +MpdOptions::MpdOptions() + : minimum_update_period(), + min_buffer_time(), + time_shift_buffer_depth(), + suggested_presentation_delay(), + max_segment_duration(), + max_subsegment_duration(), + number_of_blocks_for_bandwidth_estimation() {} + +MpdOptions::~MpdOptions() {} + +MpdBuilder::MpdBuilder(MpdType type, const MpdOptions& mpd_options) : type_(type), + options_(mpd_options), adaptation_sets_deleter_(&adaptation_sets_) {} MpdBuilder::~MpdBuilder() {} @@ -140,8 +179,7 @@ xmlDocPtr MpdBuilder::GenerateMpd() { XmlNode mpd("MPD"); AddMpdNameSpaceInfo(&mpd); - const float kMinBufferTime = 2.0f; - mpd.SetStringAttribute("minBufferTime", SecondsToXmlDuration(kMinBufferTime)); + SetMpdOptionsValues(&mpd); // Iterate thru AdaptationSets and add them to one big Period element. XmlNode period("Period"); @@ -163,6 +201,11 @@ xmlDocPtr MpdBuilder::GenerateMpd() { return NULL; } + if (type_ == kDynamic) { + // This is the only Period and it is a regular period. + period.SetStringAttribute("start", "PT0S"); + } + if (!mpd.AddChild(period.PassScopedPtr())) return NULL; @@ -171,7 +214,7 @@ xmlDocPtr MpdBuilder::GenerateMpd() { AddStaticMpdInfo(&mpd); break; case kDynamic: - NOTIMPLEMENTED() << "MPD for live is not implemented."; + AddDynamicMpdInfo(&mpd); break; default: NOTREACHED() << "Unknown MPD type: " << type_; @@ -197,6 +240,17 @@ void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) { SecondsToXmlDuration(GetStaticMpdDuration(mpd_node))); } +void MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) { + DCHECK(mpd_node); + DCHECK_EQ(MpdBuilder::kDynamic, type_); + + static const char kDynamicMpdType[] = "dynamic"; + static const char kDynamicMpdProfile[] = + "urn:mpeg:dash:profile:isoff-live:2011"; + mpd_node->SetStringAttribute("type", kDynamicMpdType); + mpd_node->SetStringAttribute("profiles", kDynamicMpdProfile); +} + float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { DCHECK(mpd_node); DCHECK_EQ(MpdBuilder::kStatic, type_); @@ -229,6 +283,62 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { return max_duration; } +void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) { + if (type_ == kStatic) { + if (!options_.availability_start_time.empty()) { + mpd->SetStringAttribute("availabilityStartTime", + options_.availability_start_time); + } + LOG_IF(WARNING, Positive(options_.minimum_update_period)) + << "minimumUpdatePeriod should not be present in 'static' profile. " + "Ignoring."; + LOG_IF(WARNING, Positive(options_.time_shift_buffer_depth)) + << "timeShiftBufferDepth will not be used for 'static' profile. " + "Ignoring."; + LOG_IF(WARNING, Positive(options_.suggested_presentation_delay)) + << "suggestedPresentationDelay will not be used for 'static' profile. " + "Ignoring."; + } else if (type_ == kDynamic) { + // 'availabilityStartTime' is required for dynamic profile, so use current + // time if not specified. + const std::string avail_start = !options_.availability_start_time.empty() + ? options_.availability_start_time + : XmlDateTimeNow(); + mpd->SetStringAttribute("availabilityStartTime", avail_start); + + if (Positive(options_.minimum_update_period)) { + mpd->SetStringAttribute( + "minimumUpdatePeriod", + SecondsToXmlDuration(options_.minimum_update_period)); + } else { + // TODO(rkuroiwa): Set minimumUpdatePeriod to some default value. + LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod " + "specified. Setting minimumUpdatePeriod to 0."; + } + + SetIfPositive( + "timeShiftBufferDepth", options_.time_shift_buffer_depth, mpd); + SetIfPositive("suggestedPresentationDelay", + options_.suggested_presentation_delay, + mpd); + } + + const double kDefaultMinBufferTime = 2.0; + const double min_buffer_time = Positive(options_.min_buffer_time) + ? options_.min_buffer_time + : kDefaultMinBufferTime; + mpd->SetStringAttribute("minBufferTime", + SecondsToXmlDuration(min_buffer_time)); + + if (!options_.availability_end_time.empty()) { + mpd->SetStringAttribute("availabilityEndTime", + options_.availability_end_time); + } + + SetIfPositive("maxSegmentDuration", options_.max_segment_duration, mpd); + SetIfPositive("maxSubsegmentDuration", options_.max_subsegment_duration, mpd); +} + AdaptationSet::AdaptationSet(uint32 adaptation_set_id, base::AtomicSequenceNumber* counter) : representations_deleter_(&representations_), @@ -283,14 +393,13 @@ xml::ScopedXmlPtr::type AdaptationSet::GetXml() { } Representation::Representation(const MediaInfo& media_info, uint32 id) - : media_info_(media_info), id_(id) {} + : media_info_(media_info), + id_(id), + bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks) {} Representation::~Representation() {} bool Representation::Init() { - if (!HasRequiredMediaInfoFields()) - return false; - codecs_ = GetCodecs(media_info_); if (codecs_.empty()) { LOG(ERROR) << "Missing codec info in MediaInfo."; @@ -331,11 +440,24 @@ void Representation::AddContentProtectionElement( RemoveDuplicateAttributes(&content_protection_elements_.back()); } -bool Representation::AddNewSegment(uint64 start_time, uint64 duration) { +void Representation::AddNewSegment(uint64 start_time, + uint64 duration, + uint64 size) { + if (start_time == 0 && duration == 0) { + LOG(WARNING) << "Got segment with start_time and duration == 0. Ignoring."; + return; + } + base::AutoLock scoped_lock(lock_); - segment_starttime_duration_pairs_.push_back( - std::pair(start_time, duration)); - return true; + if (IsContiguous(start_time, duration, size)) { + ++segment_infos_.back().repeat; + } else { + SegmentInfo s = {start_time, duration, /* Not repeat. */ 0}; + segment_infos_.push_back(s); + } + + bandwidth_estimator_.AddBlock( + size, static_cast(duration) / media_info_.reference_time_scale()); } // Uses info in |media_info_| and |content_protection_elements_| to create a @@ -346,13 +468,22 @@ bool Representation::AddNewSegment(uint64 start_time, uint64 duration) { // AddVODOnlyInfo() (Adds segment info). xml::ScopedXmlPtr::type Representation::GetXml() { base::AutoLock scoped_lock(lock_); + + if (!HasRequiredMediaInfoFields()) { + LOG(ERROR) << "MediaInfo missing required fields."; + return xml::ScopedXmlPtr::type(); + } + + const uint64 bandwidth = media_info_.has_bandwidth() + ? media_info_.bandwidth() + : bandwidth_estimator_.Estimate(); + DCHECK(!(HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_))); - DCHECK(media_info_.has_bandwidth()); RepresentationXmlNode representation; // Mandatory fields for Representation. representation.SetId(id_); - representation.SetIntegerAttribute("bandwidth", media_info_.bandwidth()); + representation.SetIntegerAttribute("bandwidth", bandwidth); representation.SetStringAttribute("codecs", codecs_); representation.SetStringAttribute("mimeType", mime_type_); @@ -384,6 +515,14 @@ xml::ScopedXmlPtr::type Representation::GetXml() { return xml::ScopedXmlPtr::type(); } + if (HasLiveOnlyFields(media_info_) && + !representation.AddLiveOnlyInfo(media_info_, segment_infos_)) { + LOG(ERROR) << "Failed to add Live info."; + return xml::ScopedXmlPtr::type(); + } + // TODO(rkuroiwa): It is likely that all representations have the exact same + // SegmentTemplate. Optimize and propagate the tag up to AdaptationSet level. + return representation.PassScopedPtr(); } @@ -393,19 +532,73 @@ bool Representation::HasRequiredMediaInfoFields() { return false; } - if (!media_info_.has_bandwidth()) { - LOG(ERROR) << "MediaInfo missing required field: bandwidth."; - return false; - } - if (!media_info_.has_container_type()) { LOG(ERROR) << "MediaInfo missing required field: container_type."; return false; } + if (HasVODOnlyFields(media_info_) && !media_info_.has_bandwidth()) { + LOG(ERROR) << "Missing 'bandwidth' field. MediaInfo requires bandwidth for " + "static profile for generating a valid MPD."; + return false; + } + + VLOG_IF(3, HasLiveOnlyFields(media_info_) && !media_info_.has_bandwidth()) + << "MediaInfo missing field 'bandwidth'. Using estimated from " + "segment size."; + return true; } +// In Debug builds, some of the irregular cases crash. It is probably a +// programming error but in production, it might not be best to stop the +// pipeline, especially for live. +bool Representation::IsContiguous(uint64 start_time, + uint64 duration, + uint64 size) const { + if (segment_infos_.empty() || segment_infos_.back().duration != duration) + return false; + + // Contiguous segment. + const SegmentInfo& previous = segment_infos_.back(); + const uint64 previous_segment_end_time = + previous.start_time + + previous.duration * (previous.repeat + 1); + if (previous_segment_end_time == start_time) + return true; + + // A gap since previous. + if (previous_segment_end_time < start_time) + return false; + + // No out of order segments. + const uint64 previous_segment_start_time = + previous.start_time + + previous.duration * previous.repeat; + if (previous_segment_start_time >= start_time) { + LOG(ERROR) << "Segments should not be out of order segment. Adding segment " + "with start_time == " << start_time + << " but the previous segment starts at " << previous.start_time + << "."; + DCHECK(false); + return false; + } + + // No overlapping segments. + const uint64 kRoundingErrorGrace = 5; + if (start_time < previous_segment_end_time - kRoundingErrorGrace) { + LOG(WARNING) + << "Segments shold not be overlapping. The new segment starts at " + << start_time << " but the previous segment ends at " + << previous_segment_end_time << "."; + DCHECK(false); + return false; + } + + // Within rounding error grace but technically not contiguous interms of MPD. + return false; +} + std::string Representation::GetVideoMimeType() const { return GetMimeType("video", media_info_.container_type()); } diff --git a/mpd/base/mpd_builder.h b/mpd/base/mpd_builder.h index 647056be57..7effabc3c6 100644 --- a/mpd/base/mpd_builder.h +++ b/mpd/base/mpd_builder.h @@ -18,8 +18,10 @@ #include "base/stl_util.h" #include "base/synchronization/lock.h" #include "mpd/base/content_protection_element.h" +#include "mpd/base/bandwidth_estimator.h" #include "mpd/base/media_info.pb.h" #include "mpd/base/mpd_utils.h" +#include "mpd/base/segment_info.h" #include "mpd/base/xml/scoped_xml_ptr.h" namespace dash_packager { @@ -30,9 +32,28 @@ class Representation; namespace xml { class XmlNode; +class RepresentationXmlNode; } // namespace xml +struct MpdOptions { + MpdOptions(); + ~MpdOptions(); + + std::string availability_start_time; + std::string availability_end_time; + double minimum_update_period; + double min_buffer_time; + double time_shift_buffer_depth; + double suggested_presentation_delay; + double max_segment_duration; + double max_subsegment_duration; + + /// Value passed to BandwidthEstimator's contructor. See BandwidthEstimator + /// for more. + int number_of_blocks_for_bandwidth_estimation; +}; + /// This class generates DASH MPDs (Media Presentation Descriptions). class MpdBuilder { public: @@ -44,7 +65,7 @@ class MpdBuilder { /// Constructs MpdBuilder. /// @param type indicates whether the MPD should be for VOD or live content /// (kStatic for VOD profile, or kDynamic for live profile). - explicit MpdBuilder(MpdType type); + MpdBuilder(MpdType type, const MpdOptions& mpd_options); ~MpdBuilder(); /// Add entry to the MPD. @@ -64,6 +85,10 @@ class MpdBuilder { MpdType type() { return type_; } private: + // DynamicMpdBuilderTest uses SetMpdOptionsValues to set availabilityStartTime + // so that the test doesn't need to depend on current time. + friend class DynamicMpdBuilderTest; + bool ToStringImpl(std::string* output); // Returns the document pointer to the MPD. This must be freed by the caller @@ -74,9 +99,19 @@ class MpdBuilder { // Adds 'static' MPD attributes and elements to |mpd_node|. This assumes that // the first child element is a Period element. void AddStaticMpdInfo(xml::XmlNode* mpd_node); + + // Same as AddStaticMpdInfo() but for 'dynamic' MPDs. + void AddDynamicMpdInfo(xml::XmlNode* mpd_node); + float GetStaticMpdDuration(xml::XmlNode* mpd_node); + // Use |options_| to set attributes for MPD. Only values that are set will be + // used, i.e. if a string field is not empty and numeric field is not 0. + // Required fields will be set with some reasonable values. + void SetMpdOptionsValues(xml::XmlNode* mpd_node); + MpdType type_; + MpdOptions options_; std::list adaptation_sets_; ::STLElementDeleter > adaptation_sets_deleter_; @@ -143,7 +178,12 @@ class AdaptationSet { /// well as optional ContentProtection elements for that stream. class Representation { public: + // TODO(rkuroiwa): Get the value from MpdOptions for constructing + // BandwidthEstimator. /// @param media_info is a MediaInfo containing information on the media. + /// @a media_info.bandwidth is required for 'static' profile. If @a + /// media_info.bandwidth is not present in 'dynamic' profile, this + /// tries to estimate it using the info passed to AddNewSegment(). /// @param representation_id is the numeric ID for the . Representation(const MediaInfo& media_info, uint32 representation_id); ~Representation(); @@ -165,8 +205,8 @@ class Representation { /// stream's time scale. /// @param duration is the duration of the segment, in units of the stream's /// time scale. - /// @return true on success, false otherwise. - bool AddNewSegment(uint64 start_time, uint64 duration); + /// @param size of the segment in bytes. + void AddNewSegment(uint64 start_time, uint64 duration, uint64 size); /// @return Copy of . xml::ScopedXmlPtr::type GetXml(); @@ -177,10 +217,16 @@ class Representation { } private: + bool AddLiveInfo(xml::RepresentationXmlNode* representation); + // Returns true if |media_info_| has required fields to generate a valid // Representation. Otherwise returns false. bool HasRequiredMediaInfoFields(); + // Return false if the segment should be considered a new segment. True if the + // segment is contiguous. + bool IsContiguous(uint64 start_time, uint64 duration, uint64 size) const; + // Note: Because 'mimeType' is a required field for a valid MPD, these return // strings. std::string GetVideoMimeType() const; @@ -188,13 +234,14 @@ class Representation { MediaInfo media_info_; std::list content_protection_elements_; - std::list > segment_starttime_duration_pairs_; + std::list segment_infos_; base::Lock lock_; const uint32 id_; std::string mime_type_; std::string codecs_; + BandwidthEstimator bandwidth_estimator_; DISALLOW_COPY_AND_ASSIGN(Representation); }; diff --git a/mpd/base/mpd_builder_unittest.cc b/mpd/base/mpd_builder_unittest.cc index b3f8d80d1a..df51f6ef9a 100644 --- a/mpd/base/mpd_builder_unittest.cc +++ b/mpd/base/mpd_builder_unittest.cc @@ -7,8 +7,11 @@ #include "base/file_util.h" #include "base/logging.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/stringprintf.h" #include "mpd/base/mpd_builder.h" +#include "mpd/base/mpd_utils.h" #include "mpd/test/mpd_builder_test_helper.h" +#include "mpd/test/xml_compare.h" #include "testing/gtest/include/gtest/gtest.h" #include "third_party/libxml/src/include/libxml/xmlstring.h" @@ -42,27 +45,11 @@ void CheckIdEqual(uint32 expected_id, T* node) { } } // namespace -TEST(AdaptationSetTest, CheckId) { - base::AtomicSequenceNumber sequence_counter; - const uint32 kAdaptationSetId = 42; - - AdaptationSet adaptation_set(kAdaptationSetId, &sequence_counter); - ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kAdaptationSetId, &adaptation_set)); -} - -TEST(RepresentationTest, CheckId) { - const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); - const uint32 kRepresentationId = 1; - - Representation representation(video_media_info, kRepresentationId); - EXPECT_TRUE(representation.Init()); - ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation)); -} - -class StaticMpdBuilderTest : public ::testing::Test { +template +class MpdBuilderTest: public ::testing::Test { public: - StaticMpdBuilderTest() : mpd_(MpdBuilder::kStatic) {} - virtual ~StaticMpdBuilderTest() {} + MpdBuilderTest() : mpd_(type, MpdOptions()), representation_() {} + virtual ~MpdBuilderTest() {} void CheckMpd(const std::string& expected_output_file) { std::string mpd_doc; @@ -74,25 +61,169 @@ class StaticMpdBuilderTest : public ::testing::Test { } protected: + void AddRepresentation(const MediaInfo& media_info) { + AdaptationSet* adaptation_set = mpd_.AddAdaptationSet(); + ASSERT_TRUE(adaptation_set); + + Representation* representation = + adaptation_set->AddRepresentation(media_info); + ASSERT_TRUE(representation); + + representation_ = representation; + } + MpdBuilder mpd_; + // We usually need only one representation. + Representation* representation_; // Owned by |mpd_|. + private: - DISALLOW_COPY_AND_ASSIGN(StaticMpdBuilderTest); + DISALLOW_COPY_AND_ASSIGN(MpdBuilderTest); }; +class StaticMpdBuilderTest : public MpdBuilderTest {}; + +class DynamicMpdBuilderTest : public MpdBuilderTest { + public: + virtual ~DynamicMpdBuilderTest() {} + + // Anchors availabilityStartTime so that the test result doesn't depend on the + // current time. + virtual void SetUp() { + mpd_.options_.availability_start_time = "2011-12-25T12:30:00"; + } + + std::string GetDefaultMediaInfo() { + const char kMediaInfo[] = + "video_info {\n" + " codec: \"avc1.010101\"\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + "}\n" + "reference_time_scale: %lu\n" + "container_type: 1\n" + "init_segment_name: \"init.mp4\"\n" + "segment_template: \"$Time$.mp4\"\n"; + + return base::StringPrintf(kMediaInfo, DefaultTimeScale()); + } + + uint64 DefaultTimeScale() const { return 1000; }; +}; + +class SegmentTemplateTest : public DynamicMpdBuilderTest { + public: + SegmentTemplateTest() + : bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks) {} + virtual ~SegmentTemplateTest() {} + + virtual void SetUp() { + DynamicMpdBuilderTest::SetUp(); + ASSERT_NO_FATAL_FAILURE(AddRepresentationWithDefaultMediaInfo()); + } + + void AddSegments(uint64 start_time, + uint64 duration, + uint64 size, + uint64 repeat) { + DCHECK(representation_); + const char kSElementTemplate[] = "\n"; + const char kSElementTemplateWithoutR[] = "\n"; + + segment_infos_for_expected_out_.push_back({start_time, duration, repeat}); + if (repeat == 0) { + expected_s_elements_ += + base::StringPrintf(kSElementTemplateWithoutR, start_time, duration); + } else { + expected_s_elements_ += + base::StringPrintf(kSElementTemplate, start_time, duration, repeat); + } + + for (uint64 i = 0; i < repeat + 1; ++i) { + representation_->AddNewSegment(start_time, duration, size); + start_time += duration; + bandwidth_estimator_.AddBlock( + size, static_cast(duration) / DefaultTimeScale()); + } + } + + protected: + void AddRepresentationWithDefaultMediaInfo() { + ASSERT_NO_FATAL_FAILURE( + AddRepresentation(ConvertToMediaInfo(GetDefaultMediaInfo()))); + } + + std::string TemplateOutputInsertSElementsAndBandwidth( + const std::string& s_elements_string, uint64 bandwidth) { + const char kOutputTemplate[] = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n%s" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; + + return base::StringPrintf( + kOutputTemplate, bandwidth, s_elements_string.data()); + } + + void CheckMpdAgainstExpectedResult() { + std::string mpd_doc; + ASSERT_TRUE(mpd_.ToString(&mpd_doc)); + ASSERT_TRUE(ValidateMpdSchema(mpd_doc)); + const std::string& expected_output = + TemplateOutputInsertSElementsAndBandwidth( + expected_s_elements_, bandwidth_estimator_.Estimate()); + ASSERT_TRUE(XmlEqual(expected_output, mpd_doc)); + } + + private: + std::list segment_infos_for_expected_out_; + std::string expected_s_elements_; + BandwidthEstimator bandwidth_estimator_; +}; + +TEST_F(StaticMpdBuilderTest, CheckAdaptationSetId) { + base::AtomicSequenceNumber sequence_counter; + const uint32 kAdaptationSetId = 42; + + AdaptationSet adaptation_set(kAdaptationSetId, &sequence_counter); + ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kAdaptationSetId, &adaptation_set)); +} + +TEST_F(StaticMpdBuilderTest, CheckRepresentationId) { + const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); + const uint32 kRepresentationId = 1; + + Representation representation(video_media_info, kRepresentationId); + EXPECT_TRUE(representation.Init()); + ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation)); +} + +// Add one video check the output. TEST_F(StaticMpdBuilderTest, Video) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); - - AdaptationSet* video_adaptation_set = mpd_.AddAdaptationSet(); - ASSERT_TRUE(video_adaptation_set); - - Representation* video_representation = - video_adaptation_set->AddRepresentation(video_media_info); - ASSERT_TRUE(video_representation); - + ASSERT_NO_FATAL_FAILURE(AddRepresentation(video_media_info)); EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputVideo1)); } +// Add both video and audio and check the output. TEST_F(StaticMpdBuilderTest, VideoAndAudio) { MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); MediaInfo audio_media_info = GetTestMediaInfo(kFileNameAudioMediaInfo1); @@ -131,4 +262,162 @@ TEST_F(StaticMpdBuilderTest, AudioChannelConfigurationWithContentProtection) { EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputEncryptedAudio)); } +// Static profile requires bandwidth to be set because it has no other way to +// get the bandwidth for the Representation. +TEST_F(StaticMpdBuilderTest, MediaInfoMissingBandwidth) { + MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); + video_media_info.clear_bandwidth(); + AddRepresentation(video_media_info); + + std::string mpd_doc; + ASSERT_FALSE(mpd_.ToString(&mpd_doc)); +} + +// Check whether the attributes are set correctly for dynamic element. +TEST_F(DynamicMpdBuilderTest, CheckMpdAttributes) { + static const char kExpectedOutput[] = + "\n" + "\n" + " \n" + "\n"; + + std::string mpd_doc; + ASSERT_TRUE(mpd_.ToString(&mpd_doc)); + ASSERT_EQ(kExpectedOutput, mpd_doc); +} + +// Estimate the bandwidth given the info from AddNewSegment(). +TEST_F(SegmentTemplateTest, OneSegmentNormal) { + const uint64 kStartTime = 0; + const uint64 kDuration = 10; + const uint64 kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + + // TODO(rkuroiwa): Clean up the test/data directory. It's a mess. + EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputDynamicNormal)); +} + +TEST_F(SegmentTemplateTest, NormalRepeatedSegmentDuration) { + const uint64 kSize = 256; + uint64 start_time = 0; + uint64 duration = 40000; + uint64 repeat = 2; + AddSegments(start_time, duration, kSize, repeat); + + start_time += duration * (repeat + 1); + duration = 54321; + repeat = 0; + AddSegments(start_time, duration, kSize, repeat); + + start_time += duration * (repeat + 1); + duration = 12345; + repeat = 0; + AddSegments(start_time, duration, kSize, repeat); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +TEST_F(SegmentTemplateTest, RepeatedSegmentsFromNonZeroStartTime) { + const uint64 kSize = 100000; + uint64 start_time = 0; + uint64 duration = 100000; + uint64 repeat = 2; + AddSegments(start_time, duration, kSize, repeat); + + start_time += duration * (repeat + 1); + duration = 20000; + repeat = 3; + AddSegments(start_time, duration, kSize, repeat); + + start_time += duration * (repeat + 1); + duration = 32123; + repeat = 3; + AddSegments(start_time, duration, kSize, repeat); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +// Segments not starting from 0. +// Start time is 10. Make sure r gets set correctly. +TEST_F(SegmentTemplateTest, NonZeroStartTime) { + const uint64 kStartTime = 10; + const uint64 kDuration = 22000; + const uint64 kSize = 123456; + const uint64 kRepeat = 1; + AddSegments(kStartTime, kDuration, kSize, kRepeat); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +// There is a gap in the segments, but still valid. +TEST_F(SegmentTemplateTest, NonContiguousLiveInfo) { + const uint64 kStartTime = 10; + const uint64 kDuration = 22000; + const uint64 kSize = 123456; + const uint64 kRepeat = 0; + AddSegments(kStartTime, kDuration, kSize, kRepeat); + + const uint64 kStartTimeOffset = 100; + AddSegments(kDuration + kStartTimeOffset, kDuration, kSize, kRepeat); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +// Add segments out of order. Segments that start before the previous segment +// cannot be added. +TEST_F(SegmentTemplateTest, OutOfOrder) { + const uint64 kEarlierStartTime = 0; + const uint64 kLaterStartTime = 1000; + const uint64 kDuration = 1000; + const uint64 kSize = 123456; + const uint64 kRepeat = 0; + + AddSegments(kLaterStartTime, kDuration, kSize, kRepeat); + EXPECT_DEBUG_DEATH(AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat), + ""); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +// No segments should be overlapping. +TEST_F(SegmentTemplateTest, OverlappingSegments) { + const uint64 kEarlierStartTime = 0; + const uint64 kDuration = 1000; + const uint64 kSize = 123456; + const uint64 kRepeat = 0; + + const uint64 kOverlappingSegmentStartTime = kDuration / 2; + CHECK_GT(kDuration, kOverlappingSegmentStartTime); + + AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat); + EXPECT_DEBUG_DEATH( + AddSegments(kOverlappingSegmentStartTime, kDuration, kSize, kRepeat), ""); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + +// Some segments can be overlapped due to rounding errors. As long as it falls +// in the range of rounding error defined inside MpdBuilder, the segment gets +// accepted. +TEST_F(SegmentTemplateTest, OverlappingSegmentsWithinErrorRange) { + const uint64 kEarlierStartTime = 0; + const uint64 kDuration = 1000; + const uint64 kSize = 123456; + const uint64 kRepeat = 0; + + const uint64 kOverlappingSegmentStartTime = kDuration - 1; + CHECK_GT(kDuration, kOverlappingSegmentStartTime); + + AddSegments(kEarlierStartTime, kDuration, kSize, kRepeat); + AddSegments(kOverlappingSegmentStartTime, kDuration, kSize, kRepeat); + + ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); +} + } // namespace dash_packager diff --git a/mpd/base/mpd_utils.cc b/mpd/base/mpd_utils.cc index f1fdc2ef31..cb599dd83c 100644 --- a/mpd/base/mpd_utils.cc +++ b/mpd/base/mpd_utils.cc @@ -6,15 +6,13 @@ #include "mpd/base/mpd_utils.h" -#include - #include "base/logging.h" #include "base/strings/string_number_conversions.h" #include "mpd/base/content_protection_element.h" +#include "mpd/base/media_info.pb.h" #include "mpd/base/xml/scoped_xml_ptr.h" #include "third_party/libxml/src/include/libxml/tree.h" - namespace { // Concatenate all the codecs in |repeated_stream_info|. @@ -83,7 +81,7 @@ std::string GetCodecs(const MediaInfo& media_info) { return ""; } -std::string SecondsToXmlDuration(float seconds) { +std::string SecondsToXmlDuration(double seconds) { return "PT" + base::DoubleToString(seconds) + "S"; } diff --git a/mpd/base/mpd_utils.h b/mpd/base/mpd_utils.h index b22969b78f..f1b491a1f8 100644 --- a/mpd/base/mpd_utils.h +++ b/mpd/base/mpd_utils.h @@ -12,12 +12,13 @@ #include #include "base/basictypes.h" -#include "mpd/base/media_info.pb.h" #include "third_party/libxml/src/include/libxml/tree.h" namespace dash_packager { +class MediaInfo; struct ContentProtectionElement; +struct SegmentInfo; bool HasVODOnlyFields(const MediaInfo& media_info); @@ -33,7 +34,7 @@ void RemoveDuplicateAttributes( // comma. std::string GetCodecs(const MediaInfo& media_info); -std::string SecondsToXmlDuration(float seconds); +std::string SecondsToXmlDuration(double seconds); // Tries to get "duration" attribute from |node|. On success |duration| is set. bool GetDurationAttribute(xmlNodePtr node, float* duration); diff --git a/mpd/base/segment_info.h b/mpd/base/segment_info.h new file mode 100644 index 0000000000..1eecb47262 --- /dev/null +++ b/mpd/base/segment_info.h @@ -0,0 +1,26 @@ +// Copyright 2014 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MPD_BASE_SEGMENT_INFO_H_ +#define MPD_BASE_SEGMENT_INFO_H_ + +namespace dash_packager { +/// Container for keeping track of information about a segment. +/// Used for keeping track of all the segments used for generating MPD with +/// dynamic profile. +struct SegmentInfo { + uint64 start_time; + uint64 duration; + // This is the number of times same duration segments are repeated not + // inclusive. In other words if this is the only one segment that starts at + // |start_time| and has |duration| but none others have |start_time| * N and + // |duration|, then this should be set to 0. The semantics is the same as S@r + // in the DASH MPD spec. + uint64 repeat; +}; +} // namespace dash_packager + +#endif // MPD_BASE_SEGMENT_INFO_H_ diff --git a/mpd/base/simple_mpd_notifier.cc b/mpd/base/simple_mpd_notifier.cc index 0aa8c5b12a..b96947a4d4 100644 --- a/mpd/base/simple_mpd_notifier.cc +++ b/mpd/base/simple_mpd_notifier.cc @@ -22,7 +22,8 @@ SimpleMpdNotifier::SimpleMpdNotifier(DashProfile dash_profile, output_path_(output_path), mpd_builder_(new MpdBuilder(dash_profile == kLiveProfile ? MpdBuilder::kDynamic - : MpdBuilder::kStatic)) { + : MpdBuilder::kStatic, + MpdOptions())) { DCHECK(dash_profile == kLiveProfile || dash_profile == kOnDemandProfile); for (size_t i = 0; i < base_urls.size(); ++i) mpd_builder_->AddBaseUrl(base_urls[i]); @@ -76,8 +77,8 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32 container_id, LOG(ERROR) << "Unexpected container_id: " << container_id; return false; } - if (!it->second->AddNewSegment(start_time, duration)) - return false; + // TODO(kqyang): AddNewSegment() requires size for the third argument. + // !it->second->AddNewSegment(start_time, duration); return WriteMpdToFile(); } diff --git a/mpd/base/xml/xml_node.cc b/mpd/base/xml/xml_node.cc index e311c96cc6..f4d9a0c50e 100644 --- a/mpd/base/xml/xml_node.cc +++ b/mpd/base/xml/xml_node.cc @@ -9,8 +9,10 @@ #include #include "base/logging.h" +#include "base/stl_util.h" #include "base/strings/string_number_conversions.h" #include "mpd/base/media_info.pb.h" +#include "mpd/base/segment_info.h" using dash_packager::xml::XmlNode; @@ -18,6 +20,8 @@ using dash_packager::MediaInfo; typedef MediaInfo::ContentProtectionXml ContentProtectionXml; typedef ContentProtectionXml::AttributeNameValuePair AttributeNameValuePair; +namespace dash_packager { + namespace { std::string RangeToString(const dash_packager::Range& range) { @@ -151,9 +155,25 @@ bool TranslateToContentProtectionXmlNode( return true; } +bool PopulateSegmentTimeline(const std::list& segment_infos, + XmlNode* segment_timeline) { + for (std::list::const_iterator it = segment_infos.begin(); + it != segment_infos.end(); + ++it) { + XmlNode* s_element = new XmlNode("S"); + s_element->SetIntegerAttribute("t", it->start_time); + s_element->SetIntegerAttribute("d", it->duration); + if (it->repeat > 0) + s_element->SetIntegerAttribute("r", it->repeat); + + CHECK(segment_timeline->AddChild(s_element->PassScopedPtr())); + } + + return true; +} + } // namespace -namespace dash_packager { namespace xml { XmlNode::XmlNode(const char* name) : node_(xmlNewNode(NULL, BAD_CAST name)) { @@ -179,13 +199,13 @@ void XmlNode::SetStringAttribute(const char* attribute_name, const std::string& attribute) { DCHECK(node_); DCHECK(attribute_name); - xmlNewProp(node_.get(), BAD_CAST attribute_name, BAD_CAST attribute.c_str()); + xmlSetProp(node_.get(), BAD_CAST attribute_name, BAD_CAST attribute.c_str()); } void XmlNode::SetIntegerAttribute(const char* attribute_name, uint64 number) { DCHECK(node_); DCHECK(attribute_name); - xmlNewProp(node_.get(), + xmlSetProp(node_.get(), BAD_CAST attribute_name, BAD_CAST (base::Uint64ToString(number).c_str())); } @@ -194,7 +214,7 @@ void XmlNode::SetFloatingPointAttribute(const char* attribute_name, double number) { DCHECK(node_); DCHECK(attribute_name); - xmlNewProp(node_.get(), + xmlSetProp(node_.get(), BAD_CAST attribute_name, BAD_CAST (base::DoubleToString(number).c_str())); } @@ -390,6 +410,43 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) { return true; } +bool RepresentationXmlNode::AddLiveOnlyInfo( + const MediaInfo& media_info, + const std::list& segment_infos) { + XmlNode segment_template("SegmentTemplate"); + if (media_info.has_reference_time_scale()) { + segment_template.SetIntegerAttribute("timescale", + media_info.reference_time_scale()); + } + + if (media_info.has_init_segment_name()) { + // The spec does not allow '$Number$' and '$Time$' in initialization + // attribute. + // TODO(rkuroiwa, kqyang): Swap this check out with a better check. These + // templates allow formatting as well. + const std::string& init_segment_name = media_info.init_segment_name(); + if (init_segment_name.find("$Number$") != std::string::npos || + init_segment_name.find("$Time$") != std::string::npos) { + LOG(ERROR) << "$Number$ and $Time$ cannot be used for " + "SegmentTemplate@initialization"; + return false; + } + + segment_template.SetStringAttribute("initialization", + media_info.init_segment_name()); + } + + if (media_info.has_segment_template()) + segment_template.SetStringAttribute("media", media_info.segment_template()); + + // TODO(rkuroiwa): Find out when a live MPD doesn't require SegmentTimeline. + XmlNode segment_timeline("SegmentTimeline"); + + return PopulateSegmentTimeline(segment_infos, &segment_timeline) && + segment_template.AddChild(segment_timeline.PassScopedPtr()) && + AddChild(segment_template.PassScopedPtr()); +} + // Find all the unique number-of-channels in |repeated_audio_info|, and make // AudioChannelConfiguration for each number-of-channels. bool RepresentationXmlNode::AddAudioChannelInfo( diff --git a/mpd/base/xml/xml_node.h b/mpd/base/xml/xml_node.h index 755839588e..d8e6ee1316 100644 --- a/mpd/base/xml/xml_node.h +++ b/mpd/base/xml/xml_node.h @@ -19,6 +19,9 @@ #include "third_party/libxml/src/include/libxml/tree.h" namespace dash_packager { + +struct SegmentInfo; + namespace xml { /// These classes are wrapper classes for XML elements for generating MPD. @@ -149,6 +152,9 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode { /// @return true on success, false otherwise. bool AddVODOnlyInfo(const MediaInfo& media_info); + bool AddLiveOnlyInfo(const MediaInfo& media_info, + const std::list& segment_infos); + private: // Add AudioChannelConfiguration elements. This will add multiple // AudioChannelConfiguration if @a repeated_audio_info contains multiple diff --git a/mpd/base/xml/xml_node_unittest.cc b/mpd/base/xml/xml_node_unittest.cc index c7d5756dcf..2ea089e35b 100644 --- a/mpd/base/xml/xml_node_unittest.cc +++ b/mpd/base/xml/xml_node_unittest.cc @@ -3,9 +3,11 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +#include #include "base/logging.h" #include "base/strings/string_util.h" +#include "mpd/base/mpd_builder.h" #include "mpd/base/xml/xml_node.h" #include "mpd/test/xml_compare.h" #include "testing/gtest/include/gtest/gtest.h" @@ -28,16 +30,59 @@ void AddAttribute(const std::string& name, attribute->set_value(value); } +std::string GetDocAsFlatString(xmlDocPtr doc) { + static const int kFlatFormat = 0; + int doc_str_size = 0; + xmlChar* doc_str = NULL; + xmlDocDumpFormatMemoryEnc(doc, &doc_str, &doc_str_size, "UTF-8", kFlatFormat); + DCHECK(doc_str); + + std::string output(doc_str, doc_str + doc_str_size); + xmlFree(doc_str); + return output; +} + ScopedXmlPtr::type MakeDoc(ScopedXmlPtr::type node) { xml::ScopedXmlPtr::type doc(xmlNewDoc(BAD_CAST "")); xmlDocSetRootElement(doc.get(), node.release()); - return doc.Pass(); } + } // namespace +class RepresentationTest : public ::testing::Test { + public: + RepresentationTest() {} + virtual ~RepresentationTest() {} + + // Ownership transfers, IOW this function will release the resource for + // |node|. Returns |node| in string format. + // You should not call this function multiple times. + std::string GetStringFormat() { + xml::ScopedXmlPtr::type doc(xmlNewDoc(BAD_CAST "")); + + // Because you cannot easily get the string format of a xmlNodePtr, it gets + // attached to a temporary xml doc. + xmlDocSetRootElement(doc.get(), representation_.Release()); + std::string doc_str = GetDocAsFlatString(doc.get()); + + // GetDocAsFlatString() adds + // + // to the first line. So this removes the first line. + const size_t first_newline_char_pos = doc_str.find('\n'); + DCHECK_NE(first_newline_char_pos, std::string::npos); + return doc_str.substr(first_newline_char_pos + 1); + } + + protected: + RepresentationXmlNode representation_; + std::list segment_infos_; +}; + // Make sure XmlEqual() is functioning correctly. -TEST(MetaTest, XmlEqual) { +// TODO(rkuroiwa): Move this to a separate file. This requires it to be TEST_F +// due to gtest /test +TEST_F(RepresentationTest, MetaTest_XmlEqual) { static const char kXml1[] = "\n" " ::type doc(MakeDoc(representation.PassScopedPtr())); + representation_.AddContentProtectionElementsFromMediaInfo(media_info)); + ScopedXmlPtr::type doc(MakeDoc(representation_.PassScopedPtr())); ASSERT_TRUE( XmlEqual(kExpectedRepresentaionString, doc.get())); } +// Some template names cannot be used for init segment name. +TEST_F(RepresentationTest, InvalidLiveInitSegmentName) { + MediaInfo media_info; + + // $NUMBER$ cannot be used for segment name. + media_info.set_init_segment_name("$Number$.mp4"); + + ASSERT_FALSE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); + + // $TIME$ as well. + media_info.set_init_segment_name("$Time$.mp4"); + ASSERT_FALSE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); + + // This should be valid. + media_info.set_init_segment_name("some_non_template_name.mp4"); + ASSERT_TRUE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); +} + } // namespace xml } // namespace dash_packager diff --git a/mpd/mpd.gyp b/mpd/mpd.gyp index 98bbabcb7e..d336e76755 100644 --- a/mpd/mpd.gyp +++ b/mpd/mpd.gyp @@ -40,6 +40,8 @@ 'target_name': 'mpd_builder', 'type': 'static_library', 'sources': [ + 'base/bandwidth_estimator.cc', + 'base/bandwidth_estimator.h', 'base/content_protection_element.cc', 'base/content_protection_element.h', 'base/mpd_builder.cc', @@ -67,6 +69,7 @@ 'target_name': 'mpd_unittest', 'type': '<(gtest_target_type)', 'sources': [ + 'base/bandwidth_estimator_test.cc', 'base/mpd_builder_unittest.cc', 'base/xml/xml_node_unittest.cc', 'test/mpd_builder_test_helper.cc', diff --git a/mpd/test/data/dynamic_normal_mpd.txt b/mpd/test/data/dynamic_normal_mpd.txt new file mode 100644 index 0000000000..c11c36823d --- /dev/null +++ b/mpd/test/data/dynamic_normal_mpd.txt @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/mpd/test/mpd_builder_test_helper.h b/mpd/test/mpd_builder_test_helper.h index 46338586f2..fdf3b521da 100644 --- a/mpd/test/mpd_builder_test_helper.h +++ b/mpd/test/mpd_builder_test_helper.h @@ -39,6 +39,8 @@ const char kFileNameExpectedMpdOutputAudio1AndVideo1[] = const char kFileNameExpectedMpdOutputEncryptedAudio[] = "encrypted_audio_media_info_expected_output.txt"; +const char kFileNameExpectedMpdOutputDynamicNormal[] = "dynamic_normal_mpd.txt"; + // Returns the path to test data with |file_name|. Use constants above to get // path to the test files. base::FilePath GetTestDataFilePath(const std::string& file_name); diff --git a/mpd/util/mpd_writer.cc b/mpd/util/mpd_writer.cc index 710cd3b70d..cdb56d0440 100644 --- a/mpd/util/mpd_writer.cc +++ b/mpd/util/mpd_writer.cc @@ -152,7 +152,7 @@ void MpdWriter::AddBaseUrl(const std::string& base_url) { bool MpdWriter::WriteMpdToString(std::string* output) { CHECK(output); - MpdBuilder mpd_builder(MpdBuilder::kStatic); + MpdBuilder mpd_builder(MpdBuilder::kStatic, MpdOptions()); for (std::list::const_iterator it = base_urls_.begin(); it != base_urls_.end(); ++it) {