From 46687700939f7d8169d65db5317d95d3c13e7edf Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Tue, 27 May 2014 15:21:42 -0700 Subject: [PATCH] Respect MPD@timeShiftBufferDepth - Only Segments with end time in range [NOW - timeShiftBufferDepth, NOW] get listed as S elements under TemplateTimeline element. Any old segments do not get listed. - Also adding tests Change-Id: I52df9acaec107610757d809ac6c9cb13592d6f37 --- mpd/base/mpd_builder.cc | 160 ++++++++++-- mpd/base/mpd_builder.h | 53 ++-- mpd/base/mpd_builder_unittest.cc | 394 ++++++++++++++++++++++++++++-- mpd/base/xml/xml_node.cc | 14 +- mpd/base/xml/xml_node.h | 5 +- mpd/base/xml/xml_node_unittest.cc | 15 +- 6 files changed, 575 insertions(+), 66 deletions(-) diff --git a/mpd/base/mpd_builder.cc b/mpd/base/mpd_builder.cc index f32e174478..024216a407 100644 --- a/mpd/base/mpd_builder.cc +++ b/mpd/base/mpd_builder.cc @@ -6,6 +6,7 @@ #include "mpd/base/mpd_builder.h" +#include #include #include "base/logging.h" @@ -109,6 +110,50 @@ void SetIfPositive(const char* attr_name, double value, XmlNode* mpd) { } } +uint32 GetTimeScale(const MediaInfo& media_info) { + if (media_info.has_reference_time_scale()) { + return media_info.reference_time_scale(); + } + + if (media_info.video_info_size() > 0) { + return media_info.video_info(0).time_scale(); + } + + if (media_info.audio_info_size() > 0) { + return media_info.audio_info(0).time_scale(); + } + + LOG(WARNING) << "No timescale specified, using 1 as timescale."; + return 1; +} + +uint64 LastSegmentStartTime(const SegmentInfo& segment_info) { + return segment_info.start_time + segment_info.duration * segment_info.repeat; +} + +// This is equal to |segment_info| end time +uint64 LastSegmentEndTime(const SegmentInfo& segment_info) { + return segment_info.start_time + + segment_info.duration * (segment_info.repeat + 1); +} + +uint64 LatestSegmentStartTime(const std::list& segments) { + DCHECK(!segments.empty()); + const SegmentInfo& latest_segment = segments.back(); + return LastSegmentStartTime(latest_segment); +} + +// Given |timeshift_limit|, finds out the number of segments that are no longer +// valid and should be removed from |segment_info|. +int SearchTimedOutRepeatIndex(uint64 timeshift_limit, + const SegmentInfo& segment_info) { + DCHECK_LE(timeshift_limit, LastSegmentEndTime(segment_info)); + if (timeshift_limit < segment_info.start_time) + return 0; + + return (timeshift_limit - segment_info.start_time) / segment_info.duration; +} + } // namespace MpdOptions::MpdOptions() @@ -124,7 +169,7 @@ MpdOptions::~MpdOptions() {} MpdBuilder::MpdBuilder(MpdType type, const MpdOptions& mpd_options) : type_(type), - options_(mpd_options), + mpd_options_(mpd_options), adaptation_sets_deleter_(&adaptation_sets_) {} MpdBuilder::~MpdBuilder() {} @@ -137,7 +182,7 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) { AdaptationSet* MpdBuilder::AddAdaptationSet() { base::AutoLock scoped_lock(lock_); scoped_ptr adaptation_set(new AdaptationSet( - adaptation_set_counter_.GetNext(), &representation_counter_)); + adaptation_set_counter_.GetNext(), mpd_options_, &representation_counter_)); DCHECK(adaptation_set); adaptation_sets_.push_back(adaptation_set.get()); @@ -285,31 +330,32 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) { if (type_ == kStatic) { - if (!options_.availability_start_time.empty()) { + if (!mpd_options_.availability_start_time.empty()) { mpd->SetStringAttribute("availabilityStartTime", - options_.availability_start_time); + mpd_options_.availability_start_time); } - LOG_IF(WARNING, Positive(options_.minimum_update_period)) + LOG_IF(WARNING, Positive(mpd_options_.minimum_update_period)) << "minimumUpdatePeriod should not be present in 'static' profile. " "Ignoring."; - LOG_IF(WARNING, Positive(options_.time_shift_buffer_depth)) + LOG_IF(WARNING, Positive(mpd_options_.time_shift_buffer_depth)) << "timeShiftBufferDepth will not be used for 'static' profile. " "Ignoring."; - LOG_IF(WARNING, Positive(options_.suggested_presentation_delay)) + LOG_IF(WARNING, Positive(mpd_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(); + const std::string avail_start = + !mpd_options_.availability_start_time.empty() + ? mpd_options_.availability_start_time + : XmlDateTimeNow(); mpd->SetStringAttribute("availabilityStartTime", avail_start); - if (Positive(options_.minimum_update_period)) { + if (Positive(mpd_options_.minimum_update_period)) { mpd->SetStringAttribute( "minimumUpdatePeriod", - SecondsToXmlDuration(options_.minimum_update_period)); + SecondsToXmlDuration(mpd_options_.minimum_update_period)); } else { // TODO(rkuroiwa): Set minimumUpdatePeriod to some default value. LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod " @@ -317,33 +363,36 @@ void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) { } SetIfPositive( - "timeShiftBufferDepth", options_.time_shift_buffer_depth, mpd); + "timeShiftBufferDepth", mpd_options_.time_shift_buffer_depth, mpd); SetIfPositive("suggestedPresentationDelay", - options_.suggested_presentation_delay, + mpd_options_.suggested_presentation_delay, mpd); } const double kDefaultMinBufferTime = 2.0; - const double min_buffer_time = Positive(options_.min_buffer_time) - ? options_.min_buffer_time + const double min_buffer_time = Positive(mpd_options_.min_buffer_time) + ? mpd_options_.min_buffer_time : kDefaultMinBufferTime; mpd->SetStringAttribute("minBufferTime", SecondsToXmlDuration(min_buffer_time)); - if (!options_.availability_end_time.empty()) { + if (!mpd_options_.availability_end_time.empty()) { mpd->SetStringAttribute("availabilityEndTime", - options_.availability_end_time); + mpd_options_.availability_end_time); } - SetIfPositive("maxSegmentDuration", options_.max_segment_duration, mpd); - SetIfPositive("maxSubsegmentDuration", options_.max_subsegment_duration, mpd); + SetIfPositive("maxSegmentDuration", mpd_options_.max_segment_duration, mpd); + SetIfPositive( + "maxSubsegmentDuration", mpd_options_.max_subsegment_duration, mpd); } AdaptationSet::AdaptationSet(uint32 adaptation_set_id, + const MpdOptions& mpd_options, base::AtomicSequenceNumber* counter) : representations_deleter_(&representations_), representation_counter_(counter), - id_(adaptation_set_id) { + id_(adaptation_set_id), + mpd_options_(mpd_options) { DCHECK(counter); } @@ -351,8 +400,8 @@ AdaptationSet::~AdaptationSet() {} Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { base::AutoLock scoped_lock(lock_); - scoped_ptr representation( - new Representation(media_info, representation_counter_->GetNext())); + scoped_ptr representation(new Representation( + media_info, mpd_options_, representation_counter_->GetNext())); if (!representation->Init()) return NULL; @@ -392,10 +441,14 @@ xml::ScopedXmlPtr::type AdaptationSet::GetXml() { return adaptation_set.PassScopedPtr(); } -Representation::Representation(const MediaInfo& media_info, uint32 id) +Representation::Representation(const MediaInfo& media_info, + const MpdOptions& mpd_options, + uint32 id) : media_info_(media_info), id_(id), - bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks) {} + bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks), + mpd_options_(mpd_options), + start_number_(1) {} Representation::~Representation() {} @@ -458,6 +511,9 @@ void Representation::AddNewSegment(uint64 start_time, bandwidth_estimator_.AddBlock( size, static_cast(duration) / media_info_.reference_time_scale()); + + SlideWindow(); + DCHECK_GE(segment_infos_.size(), 1u); } // Uses info in |media_info_| and |content_protection_elements_| to create a @@ -516,7 +572,8 @@ xml::ScopedXmlPtr::type Representation::GetXml() { } if (HasLiveOnlyFields(media_info_) && - !representation.AddLiveOnlyInfo(media_info_, segment_infos_)) { + !representation.AddLiveOnlyInfo( + media_info_, segment_infos_, start_number_)) { LOG(ERROR) << "Failed to add Live info."; return xml::ScopedXmlPtr::type(); } @@ -599,6 +656,57 @@ bool Representation::IsContiguous(uint64 start_time, return false; } +void Representation::SlideWindow() { + DCHECK(!segment_infos_.empty()); + if (mpd_options_.time_shift_buffer_depth <= 0.0) + return; + + const uint32 time_scale = GetTimeScale(media_info_); + DCHECK_GT(time_scale, 0u); + + uint64 time_shift_buffer_depth = + static_cast(mpd_options_.time_shift_buffer_depth * time_scale); + + // The start time of the latest segment is considered the current_play_time, + // and this should guarantee that the latest segment will stay in the list. + const uint64 current_play_time = LatestSegmentStartTime(segment_infos_); + if (current_play_time <= time_shift_buffer_depth) + return; + + const uint64 timeshift_limit = current_play_time - time_shift_buffer_depth; + + // First remove all the SegmentInfos that are completely out of range, by + // looking at the very last segment's end time. + std::list::iterator first = segment_infos_.begin(); + std::list::iterator last = first; + size_t num_segments_removed = 0; + for (; last != segment_infos_.end(); ++last) { + const uint64 last_segment_end_time = LastSegmentEndTime(*last); + if (timeshift_limit < last_segment_end_time) + break; + num_segments_removed += last->repeat + 1; + } + segment_infos_.erase(first, last); + start_number_ += num_segments_removed; + + // Now some segment in the first SegmentInfo should be left in the list. + SegmentInfo* first_segment_info = &segment_infos_.front(); + DCHECK_LE(timeshift_limit, LastSegmentEndTime(*first_segment_info)); + + // Identify which segments should still be in the SegmentInfo. + const int repeat_index = + SearchTimedOutRepeatIndex(timeshift_limit, *first_segment_info); + CHECK_GE(repeat_index, 0); + if (repeat_index == 0) + return; + + first_segment_info->start_time = first_segment_info->start_time + + first_segment_info->duration * repeat_index; + + first_segment_info->repeat = first_segment_info->repeat - repeat_index; + start_number_ += repeat_index; +} + 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 7effabc3c6..3cab823813 100644 --- a/mpd/base/mpd_builder.h +++ b/mpd/base/mpd_builder.h @@ -15,10 +15,11 @@ #include "base/atomic_sequence_num.h" #include "base/basictypes.h" +#include "base/gtest_prod_util.h" #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/content_protection_element.h" #include "mpd/base/media_info.pb.h" #include "mpd/base/mpd_utils.h" #include "mpd/base/segment_info.h" @@ -105,13 +106,13 @@ class MpdBuilder { float GetStaticMpdDuration(xml::XmlNode* mpd_node); - // Use |options_| to set attributes for MPD. Only values that are set will be + // Use |mpd_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_; + MpdOptions mpd_options_; std::list adaptation_sets_; ::STLElementDeleter > adaptation_sets_deleter_; @@ -128,11 +129,6 @@ class MpdBuilder { /// elements to the AdaptationSet element. class AdaptationSet { public: - /// @param adaptation_set_id is an ID number for this AdaptationSet. - /// @param representation_counter is a Counter for assigning ID numbers to - /// Representation. It can not be NULL. - AdaptationSet(uint32 adaptation_set_id, - base::AtomicSequenceNumber* representation_counter); ~AdaptationSet(); /// Create a Representation instance using @a media_info. @@ -161,6 +157,16 @@ class AdaptationSet { } private: + friend class MpdBuilder; + FRIEND_TEST_ALL_PREFIXES(StaticMpdBuilderTest, CheckAdaptationSetId); + + /// @param adaptation_set_id is an ID number for this AdaptationSet. + /// @param representation_counter is a Counter for assigning ID numbers to + /// Representation. It can not be NULL. + AdaptationSet(uint32 adaptation_set_id, + const MpdOptions& mpd_options_, + base::AtomicSequenceNumber* representation_counter); + std::list content_protection_elements_; std::list representations_; ::STLElementDeleter > representations_deleter_; @@ -170,6 +176,7 @@ class AdaptationSet { base::AtomicSequenceNumber* const representation_counter_; const uint32 id_; + const MpdOptions& mpd_options_; DISALLOW_COPY_AND_ASSIGN(AdaptationSet); }; @@ -178,14 +185,6 @@ 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(); /// Tries to initialize the instance. If this does not succeed, the instance @@ -217,6 +216,18 @@ class Representation { } private: + friend class AdaptationSet; + FRIEND_TEST_ALL_PREFIXES(StaticMpdBuilderTest, CheckRepresentationId); + + /// @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, + const MpdOptions& mpd_options, + uint32 representation_id); + bool AddLiveInfo(xml::RepresentationXmlNode* representation); // Returns true if |media_info_| has required fields to generate a valid @@ -227,6 +238,11 @@ class Representation { // segment is contiguous. bool IsContiguous(uint64 start_time, uint64 duration, uint64 size) const; + // Remove elements from |segment_infos_| if + // mpd_options_.time_shift_buffer_depth is specified. Increments + // |start_number_| by the number of segments removed. + void SlideWindow(); + // Note: Because 'mimeType' is a required field for a valid MPD, these return // strings. std::string GetVideoMimeType() const; @@ -242,6 +258,11 @@ class Representation { std::string mime_type_; std::string codecs_; BandwidthEstimator bandwidth_estimator_; + const MpdOptions& mpd_options_;; + + // startNumber attribute for SegmentTemplate. + // Starts from 1. + uint32 start_number_; DISALLOW_COPY_AND_ASSIGN(Representation); }; diff --git a/mpd/base/mpd_builder_unittest.cc b/mpd/base/mpd_builder_unittest.cc index c276006e22..975a9c36a0 100644 --- a/mpd/base/mpd_builder_unittest.cc +++ b/mpd/base/mpd_builder_unittest.cc @@ -7,6 +7,7 @@ #include "base/file_util.h" #include "base/logging.h" #include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" #include "base/strings/stringprintf.h" #include "mpd/base/mpd_builder.h" #include "mpd/base/mpd_utils.h" @@ -18,6 +19,10 @@ namespace dash_packager { namespace { +const char kSElementTemplate[] = "\n"; +const char kSElementTemplateWithoutR[] = "\n"; +const int kDefaultStartNumber = 1; + // Get 'id' attribute from |node|, convert it to std::string and convert it to a // number. void ExpectXmlElementIdEqual(xmlNodePtr node, uint32 id) { @@ -90,9 +95,11 @@ class DynamicMpdBuilderTest : public MpdBuilderTest { // 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"; + mpd_.mpd_options_.availability_start_time = "2011-12-25T12:30:00"; } + MpdOptions* mutable_mpd_options() { return &mpd_.mpd_options_; } + std::string GetDefaultMediaInfo() { const char kMediaInfo[] = "video_info {\n" @@ -109,7 +116,8 @@ class DynamicMpdBuilderTest : public MpdBuilderTest { return base::StringPrintf(kMediaInfo, DefaultTimeScale()); } - uint64 DefaultTimeScale() const { return 1000; }; + // TODO(rkuroiwa): Make this a global constant in anonymous namespace. + uint64 DefaultTimeScale() const { return 1000; }; }; class SegmentTemplateTest : public DynamicMpdBuilderTest { @@ -128,8 +136,6 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest { uint64 size, uint64 repeat) { DCHECK(representation_); - const char kSElementTemplate[] = "\n"; - const char kSElementTemplateWithoutR[] = "\n"; SegmentInfo s = {start_time, duration, repeat}; segment_infos_for_expected_out_.push_back(s); @@ -155,8 +161,8 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest { AddRepresentation(ConvertToMediaInfo(GetDefaultMediaInfo()))); } - std::string TemplateOutputInsertSElementsAndBandwidth( - const std::string& s_elements_string, uint64 bandwidth) { + std::string TemplateOutputInsertValues(const std::string& s_elements_string, + uint64 bandwidth) { const char kOutputTemplate[] = "\n" "\n" "\n"; - return base::StringPrintf( - kOutputTemplate, bandwidth, s_elements_string.data()); + return base::StringPrintf(kOutputTemplate, + bandwidth, + s_elements_string.c_str()); } void CheckMpdAgainstExpectedResult() { @@ -189,22 +196,98 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest { 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)); + TemplateOutputInsertValues(expected_s_elements_, + bandwidth_estimator_.Estimate()); + ASSERT_TRUE(XmlEqual(expected_output, mpd_doc)) + << "Expected " << expected_output << std::endl << "Actual: " << mpd_doc; } - private: std::list segment_infos_for_expected_out_; std::string expected_s_elements_; BandwidthEstimator bandwidth_estimator_; }; +class TimeShiftBufferDepthTest : public SegmentTemplateTest { + public: + TimeShiftBufferDepthTest() {} + virtual ~TimeShiftBufferDepthTest() {} + + // This function is tricky. It does not call SegmentTemplateTest::Setup() so + // that it does not automatically add a representation, that has $Time$ + // template. + virtual void SetUp() { + DynamicMpdBuilderTest::SetUp(); + + // The only diff with current GetDefaultMediaInfo() is that this uses + // $Number$ for segment template. + 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: \"$Number$.mp4\"\n"; + + const std::string& number_template_media_info = + base::StringPrintf(kMediaInfo, DefaultTimeScale()); + ASSERT_NO_FATAL_FAILURE( + AddRepresentation(ConvertToMediaInfo(number_template_media_info))); + } + + void CheckTimeShiftBufferDepthResult(const std::string& expected_s_element, + int expected_time_shift_buffer_depth, + int expected_start_number) { + const char kOutputTemplate[] = + "\n" + "\n" + " \n" + " \n" + " \n" + " \n" + " \n" + " %s\n" + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; + + std::string expected_out = + base::StringPrintf(kOutputTemplate, + expected_time_shift_buffer_depth, + bandwidth_estimator_.Estimate(), + expected_start_number, + expected_s_element.c_str()); + + std::string mpd_doc; + ASSERT_TRUE(mpd_.ToString(&mpd_doc)); + ASSERT_TRUE(ValidateMpdSchema(mpd_doc)); + ASSERT_TRUE(XmlEqual(expected_out, mpd_doc)) + << "Expected " << expected_out << std::endl << "Actual: " << mpd_doc; + } +}; + TEST_F(StaticMpdBuilderTest, CheckAdaptationSetId) { base::AtomicSequenceNumber sequence_counter; const uint32 kAdaptationSetId = 42; - AdaptationSet adaptation_set(kAdaptationSetId, &sequence_counter); + AdaptationSet adaptation_set( + kAdaptationSetId, MpdOptions(), &sequence_counter); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kAdaptationSetId, &adaptation_set)); } @@ -212,7 +295,8 @@ TEST_F(StaticMpdBuilderTest, CheckRepresentationId) { const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); const uint32 kRepresentationId = 1; - Representation representation(video_media_info, kRepresentationId); + Representation representation( + video_media_info, MpdOptions(), kRepresentationId); EXPECT_TRUE(representation.Init()); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation)); } @@ -421,4 +505,286 @@ TEST_F(SegmentTemplateTest, OverlappingSegmentsWithinErrorRange) { ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); } +// All segments have the same duration and size. +TEST_F(TimeShiftBufferDepthTest, Normal) { + const int kTimeShiftBufferDepth = 10; // 10 sec. + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + // Trick to make every segment 1 second long. + const uint64 kDuration = DefaultTimeScale(); + const uint64 kSize = 10000; + const uint64 kRepeat = 1234; + const uint64 kLength = kRepeat; + + CHECK_EQ(kDuration / DefaultTimeScale() * kRepeat, kLength); + + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + + // There should only be the last 11 segments because timeshift is 10 sec and + // each segment is 1 sec and the latest segments start time is "current + // time" i.e., the latest segment does not count as part of timeshift buffer + // depth. + // Also note that S@r + 1 is the actual number of segments. + const int kExpectedRepeatsLeft = kTimeShiftBufferDepth; + const std::string expected_s_element = + base::StringPrintf(kSElementTemplate, + kDuration * (kRepeat - kExpectedRepeatsLeft), + kDuration, + static_cast(kExpectedRepeatsLeft)); + + const int kExpectedStartNumber = kRepeat - kExpectedRepeatsLeft + 1; + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, kTimeShiftBufferDepth, kExpectedStartNumber)); +} + +// TimeShiftBufferDepth is shorter than a segment. This should not discard the +// segment that can play TimeShiftBufferDepth. +// For example if TimeShiftBufferDepth = 1 min. and a 10 min segment was just +// added. Before that 9 min segment was added. The 9 min segment should not be +// removed from the MPD. +TEST_F(TimeShiftBufferDepthTest, TimeShiftBufferDepthShorterThanSegmentLength) { + const int kTimeShiftBufferDepth = 10; // 10 sec. + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + // Each duration is a second longer than timeShiftBufferDepth. + const uint64 kDuration = DefaultTimeScale() * (kTimeShiftBufferDepth + 1); + const uint64 kSize = 10000; + const uint64 kRepeat = 1; + + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + + // The two segments should be both present. + const std::string expected_s_element = base::StringPrintf( + kSElementTemplate, kInitialStartTime, kDuration, kRepeat); + + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, kTimeShiftBufferDepth, kDefaultStartNumber)); +} + +// More generic version the normal test. +TEST_F(TimeShiftBufferDepthTest, Generic) { + const int kTimeShiftBufferDepth = 30; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 123; + const uint64 kDuration = DefaultTimeScale(); + const uint64 kSize = 10000; + const uint64 kRepeat = 1000; + + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + const uint64 first_s_element_end_time = + kInitialStartTime + kDuration * (kRepeat + 1); + + // Now add 2 kTimeShiftBufferDepth long segments. + const int kNumMoreSegments = 2; + const int kMoreSegmentsRepeat = kNumMoreSegments - 1; + const uint64 kTimeShiftBufferDepthDuration = + DefaultTimeScale() * kTimeShiftBufferDepth; + AddSegments(first_s_element_end_time, + kTimeShiftBufferDepthDuration, + kSize, + kMoreSegmentsRepeat); + + // Expect only the latest S element with 2 segments. + const std::string expected_s_element = + base::StringPrintf(kSElementTemplate, + first_s_element_end_time, + kTimeShiftBufferDepthDuration, + static_cast(kMoreSegmentsRepeat)); + + const int kExpectedRemovedSegments = kRepeat + 1; + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, + kTimeShiftBufferDepth, + kDefaultStartNumber + kExpectedRemovedSegments)); +} + +// More than 1 S element in the result. +// Adds 100 one-second segments. Then add 21 two-second segments. +// This should have all of the two-second segments and 60 one-second +// segments. Note that it expects 60 segments from the first S element because +// the most recent segment added does not count +TEST_F(TimeShiftBufferDepthTest, MoreThanOneS) { + const int kTimeShiftBufferDepth = 100; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + const uint64 kSize = 20000; + + const uint64 kOneSecondDuration = DefaultTimeScale(); + const uint64 kOneSecondSegmentRepeat = 99; + AddSegments( + kInitialStartTime, kOneSecondDuration, kSize, kOneSecondSegmentRepeat); + const uint64 first_s_element_end_time = + kInitialStartTime + kOneSecondDuration * (kOneSecondSegmentRepeat + 1); + + const uint64 kTwoSecondDuration = 2 * DefaultTimeScale(); + const uint64 kTwoSecondSegmentRepeat = 20; + AddSegments(first_s_element_end_time, + kTwoSecondDuration, + kSize, + kTwoSecondSegmentRepeat); + + const uint64 kExpectedRemovedSegments = + (kOneSecondSegmentRepeat + 1 + kTwoSecondSegmentRepeat * 2) - + kTimeShiftBufferDepth; + + std::string expected_s_element = + base::StringPrintf(kSElementTemplate, + kOneSecondDuration * kExpectedRemovedSegments, + kOneSecondDuration, + kOneSecondSegmentRepeat - kExpectedRemovedSegments); + expected_s_element += base::StringPrintf(kSElementTemplate, + first_s_element_end_time, + kTwoSecondDuration, + kTwoSecondSegmentRepeat); + + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, + kTimeShiftBufferDepth, + kDefaultStartNumber + kExpectedRemovedSegments)); +} + + +// Edge case where the last segment in S element should still be in the MPD. +// Example: +// Assuming timescale = 1 so that duration of 1 means 1 second. +// TimeShiftBufferDepth is 9 sec and we currently have +// +// +// and we add another contiguous 2 second segment. +// Then the first S element's last segment should still be in the MPD. +TEST_F(TimeShiftBufferDepthTest, UseLastSegmentInS) { + const int kTimeShiftBufferDepth = 9; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 1; + const uint64 kDuration1 = + static_cast(DefaultTimeScale() * 1.5); + const uint64 kSize = 20000; + const uint64 kRepeat1 = 1; + + AddSegments(kInitialStartTime, kDuration1, kSize, kRepeat1); + + const uint64 first_s_element_end_time = + kInitialStartTime + kDuration1 * (kRepeat1 + 1); + + const uint64 kTwoSecondDuration = 2 * DefaultTimeScale(); + const uint64 kTwoSecondSegmentRepeat = 4; + + AddSegments(first_s_element_end_time, + kTwoSecondDuration, + kSize, + kTwoSecondSegmentRepeat); + + std::string expected_s_element = base::StringPrintf( + kSElementTemplateWithoutR, + kInitialStartTime + kDuration1, // Expect one segment removed. + kDuration1); + + expected_s_element += base::StringPrintf(kSElementTemplate, + first_s_element_end_time, + kTwoSecondDuration, + kTwoSecondSegmentRepeat); + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, kTimeShiftBufferDepth, 2)); +} + +// Gap between S elements but both should be included. +TEST_F(TimeShiftBufferDepthTest, NormalGap) { + const int kTimeShiftBufferDepth = 10; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + const uint64 kDuration = DefaultTimeScale(); + const uint64 kSize = 20000; + const uint64 kRepeat = 6; + // CHECK here so that the when next S element is added with 1 segment, this S + // element doesn't go away. + CHECK_LT(kRepeat - 1u, static_cast(kTimeShiftBufferDepth)); + CHECK_EQ(kDuration, DefaultTimeScale()); + + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + + const uint64 first_s_element_end_time = + kInitialStartTime + kDuration * (kRepeat + 1); + + const uint64 gap_s_element_start_time = first_s_element_end_time + 1; + AddSegments(gap_s_element_start_time, kDuration, kSize, /* no repeat */ 0); + + std::string expected_s_element = base::StringPrintf( + kSElementTemplate, kInitialStartTime, kDuration, kRepeat); + expected_s_element += base::StringPrintf( + kSElementTemplateWithoutR, gap_s_element_start_time, kDuration); + + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, kTimeShiftBufferDepth, kDefaultStartNumber)); +} + +// Case where there is a huge gap so the first S element is removed. +TEST_F(TimeShiftBufferDepthTest, HugeGap) { + const int kTimeShiftBufferDepth = 10; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + const uint64 kDuration = DefaultTimeScale(); + const uint64 kSize = 20000; + const uint64 kRepeat = 6; + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + + const uint64 first_s_element_end_time = + kInitialStartTime + kDuration * (kRepeat + 1); + + // Big enough gap so first S element should not be there. + const uint64 gap_s_element_start_time = + first_s_element_end_time + + (kTimeShiftBufferDepth + 1) * DefaultTimeScale(); + const uint64 kSecondSElementRepeat = 9; + COMPILE_ASSERT( + kSecondSElementRepeat < static_cast(kTimeShiftBufferDepth), + second_s_element_repeat_must_be_less_than_time_shift_buffer_depth); + AddSegments(gap_s_element_start_time, kDuration, kSize, kSecondSElementRepeat); + + std::string expected_s_element = base::StringPrintf(kSElementTemplate, + gap_s_element_start_time, + kDuration, + kSecondSElementRepeat); + const int kExpectedRemovedSegments = kRepeat + 1; + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, + kTimeShiftBufferDepth, + kDefaultStartNumber + kExpectedRemovedSegments)); +} + +// Check if startNumber is working correctly. +TEST_F(TimeShiftBufferDepthTest, ManySegments) { + const int kTimeShiftBufferDepth = 1; + mutable_mpd_options()->time_shift_buffer_depth = kTimeShiftBufferDepth; + + const uint64 kInitialStartTime = 0; + const uint64 kDuration = DefaultTimeScale(); + const uint64 kSize = 20000; + const uint64 kRepeat = 10000; + const uint64 kTotalNumSegments = kRepeat + 1; + AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + + const int kExpectedSegmentsLeft = kTimeShiftBufferDepth + 1; + const int kExpectedSegmentsRepeat = kExpectedSegmentsLeft - 1; + const int kExpectedRemovedSegments = + kTotalNumSegments - kExpectedSegmentsLeft; + + std::string expected_s_element = + base::StringPrintf(kSElementTemplate, + kExpectedRemovedSegments * kDuration, + kDuration, + static_cast(kExpectedSegmentsRepeat)); + + ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult( + expected_s_element, + kTimeShiftBufferDepth, + kDefaultStartNumber + kExpectedRemovedSegments)); +} + } // namespace dash_packager diff --git a/mpd/base/xml/xml_node.cc b/mpd/base/xml/xml_node.cc index f4d9a0c50e..89e2bf6456 100644 --- a/mpd/base/xml/xml_node.cc +++ b/mpd/base/xml/xml_node.cc @@ -412,7 +412,8 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) { bool RepresentationXmlNode::AddLiveOnlyInfo( const MediaInfo& media_info, - const std::list& segment_infos) { + const std::list& segment_infos, + uint32 start_number) { XmlNode segment_template("SegmentTemplate"); if (media_info.has_reference_time_scale()) { segment_template.SetIntegerAttribute("timescale", @@ -431,14 +432,21 @@ bool RepresentationXmlNode::AddLiveOnlyInfo( "SegmentTemplate@initialization"; return false; } - segment_template.SetStringAttribute("initialization", media_info.init_segment_name()); } - if (media_info.has_segment_template()) + if (media_info.has_segment_template()) { segment_template.SetStringAttribute("media", media_info.segment_template()); + // TODO(rkuroiwa): Need a better check. $$Number is legitimate but not a + // template. + if (media_info.segment_template().find("$Number") != std::string::npos) { + DCHECK_GE(start_number, 1u); + segment_template.SetIntegerAttribute("startNumber", start_number); + } + } + // TODO(rkuroiwa): Find out when a live MPD doesn't require SegmentTimeline. XmlNode segment_timeline("SegmentTimeline"); diff --git a/mpd/base/xml/xml_node.h b/mpd/base/xml/xml_node.h index d8e6ee1316..10c073d6e9 100644 --- a/mpd/base/xml/xml_node.h +++ b/mpd/base/xml/xml_node.h @@ -152,8 +152,11 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode { /// @return true on success, false otherwise. bool AddVODOnlyInfo(const MediaInfo& media_info); + /// @param segment_infos is a set of SegmentInfos. This method assumes that + /// SegmentInfos are sorted by its start time. bool AddLiveOnlyInfo(const MediaInfo& media_info, - const std::list& segment_infos); + const std::list& segment_infos, + uint32 start_number); private: // Add AudioChannelConfiguration elements. This will add multiple diff --git a/mpd/base/xml/xml_node_unittest.cc b/mpd/base/xml/xml_node_unittest.cc index 2ea089e35b..575db7a5c4 100644 --- a/mpd/base/xml/xml_node_unittest.cc +++ b/mpd/base/xml/xml_node_unittest.cc @@ -201,19 +201,22 @@ TEST_F(RepresentationTest, AddContentProtectionXml) { // Some template names cannot be used for init segment name. TEST_F(RepresentationTest, InvalidLiveInitSegmentName) { MediaInfo media_info; + const uint32 kDefaultStartNumber = 1; - // $NUMBER$ cannot be used for segment name. + // $Number$ cannot be used for segment name. media_info.set_init_segment_name("$Number$.mp4"); + ASSERT_FALSE(representation_.AddLiveOnlyInfo( + media_info, segment_infos_, kDefaultStartNumber)); - ASSERT_FALSE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); - - // $TIME$ as well. + // $Time$ as well. media_info.set_init_segment_name("$Time$.mp4"); - ASSERT_FALSE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); + ASSERT_FALSE(representation_.AddLiveOnlyInfo( + media_info, segment_infos_, kDefaultStartNumber)); // This should be valid. media_info.set_init_segment_name("some_non_template_name.mp4"); - ASSERT_TRUE(representation_.AddLiveOnlyInfo(media_info, segment_infos_)); + ASSERT_TRUE(representation_.AddLiveOnlyInfo( + media_info, segment_infos_, kDefaultStartNumber)); } } // namespace xml