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
This commit is contained in:
Rintaro Kuroiwa 2014-05-27 15:21:42 -07:00
parent 1927109818
commit 4668770093
6 changed files with 575 additions and 66 deletions

View File

@ -6,6 +6,7 @@
#include "mpd/base/mpd_builder.h" #include "mpd/base/mpd_builder.h"
#include <list>
#include <string> #include <string>
#include "base/logging.h" #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<SegmentInfo>& 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 } // namespace
MpdOptions::MpdOptions() MpdOptions::MpdOptions()
@ -124,7 +169,7 @@ MpdOptions::~MpdOptions() {}
MpdBuilder::MpdBuilder(MpdType type, const MpdOptions& mpd_options) MpdBuilder::MpdBuilder(MpdType type, const MpdOptions& mpd_options)
: type_(type), : type_(type),
options_(mpd_options), mpd_options_(mpd_options),
adaptation_sets_deleter_(&adaptation_sets_) {} adaptation_sets_deleter_(&adaptation_sets_) {}
MpdBuilder::~MpdBuilder() {} MpdBuilder::~MpdBuilder() {}
@ -137,7 +182,7 @@ void MpdBuilder::AddBaseUrl(const std::string& base_url) {
AdaptationSet* MpdBuilder::AddAdaptationSet() { AdaptationSet* MpdBuilder::AddAdaptationSet() {
base::AutoLock scoped_lock(lock_); base::AutoLock scoped_lock(lock_);
scoped_ptr<AdaptationSet> adaptation_set(new AdaptationSet( scoped_ptr<AdaptationSet> adaptation_set(new AdaptationSet(
adaptation_set_counter_.GetNext(), &representation_counter_)); adaptation_set_counter_.GetNext(), mpd_options_, &representation_counter_));
DCHECK(adaptation_set); DCHECK(adaptation_set);
adaptation_sets_.push_back(adaptation_set.get()); adaptation_sets_.push_back(adaptation_set.get());
@ -285,31 +330,32 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) {
void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) { void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) {
if (type_ == kStatic) { if (type_ == kStatic) {
if (!options_.availability_start_time.empty()) { if (!mpd_options_.availability_start_time.empty()) {
mpd->SetStringAttribute("availabilityStartTime", 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. " << "minimumUpdatePeriod should not be present in 'static' profile. "
"Ignoring."; "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. " << "timeShiftBufferDepth will not be used for 'static' profile. "
"Ignoring."; "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. " << "suggestedPresentationDelay will not be used for 'static' profile. "
"Ignoring."; "Ignoring.";
} else if (type_ == kDynamic) { } else if (type_ == kDynamic) {
// 'availabilityStartTime' is required for dynamic profile, so use current // 'availabilityStartTime' is required for dynamic profile, so use current
// time if not specified. // time if not specified.
const std::string avail_start = !options_.availability_start_time.empty() const std::string avail_start =
? options_.availability_start_time !mpd_options_.availability_start_time.empty()
: XmlDateTimeNow(); ? mpd_options_.availability_start_time
: XmlDateTimeNow();
mpd->SetStringAttribute("availabilityStartTime", avail_start); mpd->SetStringAttribute("availabilityStartTime", avail_start);
if (Positive(options_.minimum_update_period)) { if (Positive(mpd_options_.minimum_update_period)) {
mpd->SetStringAttribute( mpd->SetStringAttribute(
"minimumUpdatePeriod", "minimumUpdatePeriod",
SecondsToXmlDuration(options_.minimum_update_period)); SecondsToXmlDuration(mpd_options_.minimum_update_period));
} else { } else {
// TODO(rkuroiwa): Set minimumUpdatePeriod to some default value. // TODO(rkuroiwa): Set minimumUpdatePeriod to some default value.
LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod " LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod "
@ -317,33 +363,36 @@ void MpdBuilder::SetMpdOptionsValues(XmlNode* mpd) {
} }
SetIfPositive( SetIfPositive(
"timeShiftBufferDepth", options_.time_shift_buffer_depth, mpd); "timeShiftBufferDepth", mpd_options_.time_shift_buffer_depth, mpd);
SetIfPositive("suggestedPresentationDelay", SetIfPositive("suggestedPresentationDelay",
options_.suggested_presentation_delay, mpd_options_.suggested_presentation_delay,
mpd); mpd);
} }
const double kDefaultMinBufferTime = 2.0; const double kDefaultMinBufferTime = 2.0;
const double min_buffer_time = Positive(options_.min_buffer_time) const double min_buffer_time = Positive(mpd_options_.min_buffer_time)
? options_.min_buffer_time ? mpd_options_.min_buffer_time
: kDefaultMinBufferTime; : kDefaultMinBufferTime;
mpd->SetStringAttribute("minBufferTime", mpd->SetStringAttribute("minBufferTime",
SecondsToXmlDuration(min_buffer_time)); SecondsToXmlDuration(min_buffer_time));
if (!options_.availability_end_time.empty()) { if (!mpd_options_.availability_end_time.empty()) {
mpd->SetStringAttribute("availabilityEndTime", mpd->SetStringAttribute("availabilityEndTime",
options_.availability_end_time); mpd_options_.availability_end_time);
} }
SetIfPositive("maxSegmentDuration", options_.max_segment_duration, mpd); SetIfPositive("maxSegmentDuration", mpd_options_.max_segment_duration, mpd);
SetIfPositive("maxSubsegmentDuration", options_.max_subsegment_duration, mpd); SetIfPositive(
"maxSubsegmentDuration", mpd_options_.max_subsegment_duration, mpd);
} }
AdaptationSet::AdaptationSet(uint32 adaptation_set_id, AdaptationSet::AdaptationSet(uint32 adaptation_set_id,
const MpdOptions& mpd_options,
base::AtomicSequenceNumber* counter) base::AtomicSequenceNumber* counter)
: representations_deleter_(&representations_), : representations_deleter_(&representations_),
representation_counter_(counter), representation_counter_(counter),
id_(adaptation_set_id) { id_(adaptation_set_id),
mpd_options_(mpd_options) {
DCHECK(counter); DCHECK(counter);
} }
@ -351,8 +400,8 @@ AdaptationSet::~AdaptationSet() {}
Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) {
base::AutoLock scoped_lock(lock_); base::AutoLock scoped_lock(lock_);
scoped_ptr<Representation> representation( scoped_ptr<Representation> representation(new Representation(
new Representation(media_info, representation_counter_->GetNext())); media_info, mpd_options_, representation_counter_->GetNext()));
if (!representation->Init()) if (!representation->Init())
return NULL; return NULL;
@ -392,10 +441,14 @@ xml::ScopedXmlPtr<xmlNode>::type AdaptationSet::GetXml() {
return adaptation_set.PassScopedPtr(); 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), : media_info_(media_info),
id_(id), id_(id),
bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks) {} bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks),
mpd_options_(mpd_options),
start_number_(1) {}
Representation::~Representation() {} Representation::~Representation() {}
@ -458,6 +511,9 @@ void Representation::AddNewSegment(uint64 start_time,
bandwidth_estimator_.AddBlock( bandwidth_estimator_.AddBlock(
size, static_cast<double>(duration) / media_info_.reference_time_scale()); size, static_cast<double>(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 // Uses info in |media_info_| and |content_protection_elements_| to create a
@ -516,7 +572,8 @@ xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
} }
if (HasLiveOnlyFields(media_info_) && 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."; LOG(ERROR) << "Failed to add Live info.";
return xml::ScopedXmlPtr<xmlNode>::type(); return xml::ScopedXmlPtr<xmlNode>::type();
} }
@ -599,6 +656,57 @@ bool Representation::IsContiguous(uint64 start_time,
return false; 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<uint64>(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<SegmentInfo>::iterator first = segment_infos_.begin();
std::list<SegmentInfo>::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 { std::string Representation::GetVideoMimeType() const {
return GetMimeType("video", media_info_.container_type()); return GetMimeType("video", media_info_.container_type());
} }

View File

@ -15,10 +15,11 @@
#include "base/atomic_sequence_num.h" #include "base/atomic_sequence_num.h"
#include "base/basictypes.h" #include "base/basictypes.h"
#include "base/gtest_prod_util.h"
#include "base/stl_util.h" #include "base/stl_util.h"
#include "base/synchronization/lock.h" #include "base/synchronization/lock.h"
#include "mpd/base/content_protection_element.h"
#include "mpd/base/bandwidth_estimator.h" #include "mpd/base/bandwidth_estimator.h"
#include "mpd/base/content_protection_element.h"
#include "mpd/base/media_info.pb.h" #include "mpd/base/media_info.pb.h"
#include "mpd/base/mpd_utils.h" #include "mpd/base/mpd_utils.h"
#include "mpd/base/segment_info.h" #include "mpd/base/segment_info.h"
@ -105,13 +106,13 @@ class MpdBuilder {
float GetStaticMpdDuration(xml::XmlNode* mpd_node); 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. // 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. // Required fields will be set with some reasonable values.
void SetMpdOptionsValues(xml::XmlNode* mpd_node); void SetMpdOptionsValues(xml::XmlNode* mpd_node);
MpdType type_; MpdType type_;
MpdOptions options_; MpdOptions mpd_options_;
std::list<AdaptationSet*> adaptation_sets_; std::list<AdaptationSet*> adaptation_sets_;
::STLElementDeleter<std::list<AdaptationSet*> > adaptation_sets_deleter_; ::STLElementDeleter<std::list<AdaptationSet*> > adaptation_sets_deleter_;
@ -128,11 +129,6 @@ class MpdBuilder {
/// <ContentProtection> elements to the AdaptationSet element. /// <ContentProtection> elements to the AdaptationSet element.
class AdaptationSet { class AdaptationSet {
public: 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(); ~AdaptationSet();
/// Create a Representation instance using @a media_info. /// Create a Representation instance using @a media_info.
@ -161,6 +157,16 @@ class AdaptationSet {
} }
private: 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<ContentProtectionElement> content_protection_elements_; std::list<ContentProtectionElement> content_protection_elements_;
std::list<Representation*> representations_; std::list<Representation*> representations_;
::STLElementDeleter<std::list<Representation*> > representations_deleter_; ::STLElementDeleter<std::list<Representation*> > representations_deleter_;
@ -170,6 +176,7 @@ class AdaptationSet {
base::AtomicSequenceNumber* const representation_counter_; base::AtomicSequenceNumber* const representation_counter_;
const uint32 id_; const uint32 id_;
const MpdOptions& mpd_options_;
DISALLOW_COPY_AND_ASSIGN(AdaptationSet); DISALLOW_COPY_AND_ASSIGN(AdaptationSet);
}; };
@ -178,14 +185,6 @@ class AdaptationSet {
/// well as optional ContentProtection elements for that stream. /// well as optional ContentProtection elements for that stream.
class Representation { class Representation {
public: 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>.
Representation(const MediaInfo& media_info, uint32 representation_id);
~Representation(); ~Representation();
/// Tries to initialize the instance. If this does not succeed, the instance /// Tries to initialize the instance. If this does not succeed, the instance
@ -217,6 +216,18 @@ class Representation {
} }
private: 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>.
Representation(const MediaInfo& media_info,
const MpdOptions& mpd_options,
uint32 representation_id);
bool AddLiveInfo(xml::RepresentationXmlNode* representation); bool AddLiveInfo(xml::RepresentationXmlNode* representation);
// Returns true if |media_info_| has required fields to generate a valid // Returns true if |media_info_| has required fields to generate a valid
@ -227,6 +238,11 @@ class Representation {
// segment is contiguous. // segment is contiguous.
bool IsContiguous(uint64 start_time, uint64 duration, uint64 size) const; 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 // Note: Because 'mimeType' is a required field for a valid MPD, these return
// strings. // strings.
std::string GetVideoMimeType() const; std::string GetVideoMimeType() const;
@ -242,6 +258,11 @@ class Representation {
std::string mime_type_; std::string mime_type_;
std::string codecs_; std::string codecs_;
BandwidthEstimator bandwidth_estimator_; BandwidthEstimator bandwidth_estimator_;
const MpdOptions& mpd_options_;;
// startNumber attribute for SegmentTemplate.
// Starts from 1.
uint32 start_number_;
DISALLOW_COPY_AND_ASSIGN(Representation); DISALLOW_COPY_AND_ASSIGN(Representation);
}; };

View File

@ -7,6 +7,7 @@
#include "base/file_util.h" #include "base/file_util.h"
#include "base/logging.h" #include "base/logging.h"
#include "base/strings/string_number_conversions.h" #include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h" #include "base/strings/stringprintf.h"
#include "mpd/base/mpd_builder.h" #include "mpd/base/mpd_builder.h"
#include "mpd/base/mpd_utils.h" #include "mpd/base/mpd_utils.h"
@ -18,6 +19,10 @@
namespace dash_packager { namespace dash_packager {
namespace { namespace {
const char kSElementTemplate[] = "<S t=\"%lu\" d=\"%lu\" r=\"%lu\"/>\n";
const char kSElementTemplateWithoutR[] = "<S t=\"%lu\" d=\"%lu\"/>\n";
const int kDefaultStartNumber = 1;
// Get 'id' attribute from |node|, convert it to std::string and convert it to a // Get 'id' attribute from |node|, convert it to std::string and convert it to a
// number. // number.
void ExpectXmlElementIdEqual(xmlNodePtr node, uint32 id) { void ExpectXmlElementIdEqual(xmlNodePtr node, uint32 id) {
@ -90,9 +95,11 @@ class DynamicMpdBuilderTest : public MpdBuilderTest<MpdBuilder::kDynamic> {
// Anchors availabilityStartTime so that the test result doesn't depend on the // Anchors availabilityStartTime so that the test result doesn't depend on the
// current time. // current time.
virtual void SetUp() { 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() { std::string GetDefaultMediaInfo() {
const char kMediaInfo[] = const char kMediaInfo[] =
"video_info {\n" "video_info {\n"
@ -109,7 +116,8 @@ class DynamicMpdBuilderTest : public MpdBuilderTest<MpdBuilder::kDynamic> {
return base::StringPrintf(kMediaInfo, DefaultTimeScale()); 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 { class SegmentTemplateTest : public DynamicMpdBuilderTest {
@ -128,8 +136,6 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest {
uint64 size, uint64 size,
uint64 repeat) { uint64 repeat) {
DCHECK(representation_); DCHECK(representation_);
const char kSElementTemplate[] = "<S t=\"%lu\" d=\"%lu\" r=\"%lu\"/>\n";
const char kSElementTemplateWithoutR[] = "<S t=\"%lu\" d=\"%lu\"/>\n";
SegmentInfo s = {start_time, duration, repeat}; SegmentInfo s = {start_time, duration, repeat};
segment_infos_for_expected_out_.push_back(s); segment_infos_for_expected_out_.push_back(s);
@ -155,8 +161,8 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest {
AddRepresentation(ConvertToMediaInfo(GetDefaultMediaInfo()))); AddRepresentation(ConvertToMediaInfo(GetDefaultMediaInfo())));
} }
std::string TemplateOutputInsertSElementsAndBandwidth( std::string TemplateOutputInsertValues(const std::string& s_elements_string,
const std::string& s_elements_string, uint64 bandwidth) { uint64 bandwidth) {
const char kOutputTemplate[] = const char kOutputTemplate[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<MPD xmlns=\"urn:mpeg:DASH:schema:MPD:2011\" " "<MPD xmlns=\"urn:mpeg:DASH:schema:MPD:2011\" "
@ -180,8 +186,9 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest {
" </Period>\n" " </Period>\n"
"</MPD>\n"; "</MPD>\n";
return base::StringPrintf( return base::StringPrintf(kOutputTemplate,
kOutputTemplate, bandwidth, s_elements_string.data()); bandwidth,
s_elements_string.c_str());
} }
void CheckMpdAgainstExpectedResult() { void CheckMpdAgainstExpectedResult() {
@ -189,22 +196,98 @@ class SegmentTemplateTest : public DynamicMpdBuilderTest {
ASSERT_TRUE(mpd_.ToString(&mpd_doc)); ASSERT_TRUE(mpd_.ToString(&mpd_doc));
ASSERT_TRUE(ValidateMpdSchema(mpd_doc)); ASSERT_TRUE(ValidateMpdSchema(mpd_doc));
const std::string& expected_output = const std::string& expected_output =
TemplateOutputInsertSElementsAndBandwidth( TemplateOutputInsertValues(expected_s_elements_,
expected_s_elements_, bandwidth_estimator_.Estimate()); bandwidth_estimator_.Estimate());
ASSERT_TRUE(XmlEqual(expected_output, mpd_doc)); ASSERT_TRUE(XmlEqual(expected_output, mpd_doc))
<< "Expected " << expected_output << std::endl << "Actual: " << mpd_doc;
} }
private:
std::list<SegmentInfo> segment_infos_for_expected_out_; std::list<SegmentInfo> segment_infos_for_expected_out_;
std::string expected_s_elements_; std::string expected_s_elements_;
BandwidthEstimator bandwidth_estimator_; 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[] =
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<MPD xmlns=\"urn:mpeg:DASH:schema:MPD:2011\" "
"xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" "
"xmlns:xlink=\"http://www.w3.org/1999/xlink\" "
"xsi:schemaLocation=\"urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd\" "
"availabilityStartTime=\"2011-12-25T12:30:00\" minBufferTime=\"PT2S\" "
"type=\"dynamic\" profiles=\"urn:mpeg:dash:profile:isoff-live:2011\" "
"timeShiftBufferDepth=\"PT%dS\">\n"
" <Period start=\"PT0S\">\n"
" <AdaptationSet id=\"0\">\n"
" <Representation id=\"0\" bandwidth=\"%lu\" "
"codecs=\"avc1.010101\" mimeType=\"video/mp4\" width=\"720\" "
"height=\"480\">\n"
" <SegmentTemplate timescale=\"1000\" "
"initialization=\"init.mp4\" media=\"$Number$.mp4\" "
"startNumber=\"%d\">\n"
" <SegmentTimeline>\n"
" %s\n"
" </SegmentTimeline>\n"
" </SegmentTemplate>\n"
" </Representation>\n"
" </AdaptationSet>\n"
" </Period>\n"
"</MPD>\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) { TEST_F(StaticMpdBuilderTest, CheckAdaptationSetId) {
base::AtomicSequenceNumber sequence_counter; base::AtomicSequenceNumber sequence_counter;
const uint32 kAdaptationSetId = 42; 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)); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kAdaptationSetId, &adaptation_set));
} }
@ -212,7 +295,8 @@ TEST_F(StaticMpdBuilderTest, CheckRepresentationId) {
const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); const MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1);
const uint32 kRepresentationId = 1; const uint32 kRepresentationId = 1;
Representation representation(video_media_info, kRepresentationId); Representation representation(
video_media_info, MpdOptions(), kRepresentationId);
EXPECT_TRUE(representation.Init()); EXPECT_TRUE(representation.Init());
ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation)); ASSERT_NO_FATAL_FAILURE(CheckIdEqual(kRepresentationId, &representation));
} }
@ -421,4 +505,286 @@ TEST_F(SegmentTemplateTest, OverlappingSegmentsWithinErrorRange) {
ASSERT_NO_FATAL_FAILURE(CheckMpdAgainstExpectedResult()); 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<uint64>(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<uint64>(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
// <S t=0 d=1.5 r=1 />
// <S t=3 d=2 r=3 />
// 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<uint64>(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<uint64>(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<uint64>(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<uint64>(kExpectedSegmentsRepeat));
ASSERT_NO_FATAL_FAILURE(CheckTimeShiftBufferDepthResult(
expected_s_element,
kTimeShiftBufferDepth,
kDefaultStartNumber + kExpectedRemovedSegments));
}
} // namespace dash_packager } // namespace dash_packager

View File

@ -412,7 +412,8 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
bool RepresentationXmlNode::AddLiveOnlyInfo( bool RepresentationXmlNode::AddLiveOnlyInfo(
const MediaInfo& media_info, const MediaInfo& media_info,
const std::list<SegmentInfo>& segment_infos) { const std::list<SegmentInfo>& segment_infos,
uint32 start_number) {
XmlNode segment_template("SegmentTemplate"); XmlNode segment_template("SegmentTemplate");
if (media_info.has_reference_time_scale()) { if (media_info.has_reference_time_scale()) {
segment_template.SetIntegerAttribute("timescale", segment_template.SetIntegerAttribute("timescale",
@ -431,14 +432,21 @@ bool RepresentationXmlNode::AddLiveOnlyInfo(
"SegmentTemplate@initialization"; "SegmentTemplate@initialization";
return false; return false;
} }
segment_template.SetStringAttribute("initialization", segment_template.SetStringAttribute("initialization",
media_info.init_segment_name()); 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()); 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. // TODO(rkuroiwa): Find out when a live MPD doesn't require SegmentTimeline.
XmlNode segment_timeline("SegmentTimeline"); XmlNode segment_timeline("SegmentTimeline");

View File

@ -152,8 +152,11 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode {
/// @return true on success, false otherwise. /// @return true on success, false otherwise.
bool AddVODOnlyInfo(const MediaInfo& media_info); 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, bool AddLiveOnlyInfo(const MediaInfo& media_info,
const std::list<SegmentInfo>& segment_infos); const std::list<SegmentInfo>& segment_infos,
uint32 start_number);
private: private:
// Add AudioChannelConfiguration elements. This will add multiple // Add AudioChannelConfiguration elements. This will add multiple

View File

@ -201,19 +201,22 @@ TEST_F(RepresentationTest, AddContentProtectionXml) {
// Some template names cannot be used for init segment name. // Some template names cannot be used for init segment name.
TEST_F(RepresentationTest, InvalidLiveInitSegmentName) { TEST_F(RepresentationTest, InvalidLiveInitSegmentName) {
MediaInfo media_info; 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"); 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"); 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. // This should be valid.
media_info.set_init_segment_name("some_non_template_name.mp4"); 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 } // namespace xml