MpdBuilder Live profile
Add BandwidthEstimator to estimate the required bandwidth for the segments. Also includes unit test for the change. Change-Id: I28262424e2ed6ceebdf81e2b11dcd67feba1d68c
This commit is contained in:
parent
5b1980651f
commit
4a0193a816
|
@ -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 <cmath>
|
||||
|
||||
#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<int>(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<int>(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<int>(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<uint64>(ceil(num_blocks / harmonic_mean_denominator_));
|
||||
}
|
|
@ -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 <list>
|
||||
|
||||
#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<double> history_;
|
||||
};
|
||||
|
||||
#endif // MPD_BASE_BANDWIDTH_ESTIMATOR_H_
|
|
@ -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 <cmath>
|
||||
|
||||
#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
|
|
@ -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<xmlNode>::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<uint64, uint64>(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<double>(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<xmlNode>::type Representation::GetXml() {
|
||||
base::AutoLock scoped_lock(lock_);
|
||||
|
||||
if (!HasRequiredMediaInfoFields()) {
|
||||
LOG(ERROR) << "MediaInfo missing required fields.";
|
||||
return xml::ScopedXmlPtr<xmlNode>::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<xmlNode>::type Representation::GetXml() {
|
|||
return xml::ScopedXmlPtr<xmlNode>::type();
|
||||
}
|
||||
|
||||
if (HasLiveOnlyFields(media_info_) &&
|
||||
!representation.AddLiveOnlyInfo(media_info_, segment_infos_)) {
|
||||
LOG(ERROR) << "Failed to add Live info.";
|
||||
return xml::ScopedXmlPtr<xmlNode>::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());
|
||||
}
|
||||
|
|
|
@ -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 <BaseURL> 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<AdaptationSet*> adaptation_sets_;
|
||||
::STLElementDeleter<std::list<AdaptationSet*> > 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>.
|
||||
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 <Representation>.
|
||||
xml::ScopedXmlPtr<xmlNode>::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<ContentProtectionElement> content_protection_elements_;
|
||||
std::list<std::pair<uint64, uint64> > segment_starttime_duration_pairs_;
|
||||
std::list<SegmentInfo> segment_infos_;
|
||||
|
||||
base::Lock lock_;
|
||||
|
||||
const uint32 id_;
|
||||
std::string mime_type_;
|
||||
std::string codecs_;
|
||||
BandwidthEstimator bandwidth_estimator_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Representation);
|
||||
};
|
||||
|
|
|
@ -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 <MpdBuilder::MpdType type>
|
||||
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<MpdBuilder::kStatic> {};
|
||||
|
||||
class DynamicMpdBuilderTest : public MpdBuilderTest<MpdBuilder::kDynamic> {
|
||||
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[] = "<S t=\"%lu\" d=\"%lu\" r=\"%lu\"/>\n";
|
||||
const char kSElementTemplateWithoutR[] = "<S t=\"%lu\" d=\"%lu\"/>\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<double>(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[] =
|
||||
"<?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\">\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=\"$Time$.mp4\">\n"
|
||||
" <SegmentTimeline>\n%s"
|
||||
" </SegmentTimeline>\n"
|
||||
" </SegmentTemplate>\n"
|
||||
" </Representation>\n"
|
||||
" </AdaptationSet>\n"
|
||||
" </Period>\n"
|
||||
"</MPD>\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<SegmentInfo> 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 <MPD> element.
|
||||
TEST_F(DynamicMpdBuilderTest, CheckMpdAttributes) {
|
||||
static const char kExpectedOutput[] =
|
||||
"<?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\">\n"
|
||||
" <Period start=\"PT0S\"/>\n"
|
||||
"</MPD>\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
|
||||
|
|
|
@ -6,15 +6,13 @@
|
|||
|
||||
#include "mpd/base/mpd_utils.h"
|
||||
|
||||
#include <set>
|
||||
|
||||
#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";
|
||||
}
|
||||
|
||||
|
|
|
@ -12,12 +12,13 @@
|
|||
#include <string>
|
||||
|
||||
#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);
|
||||
|
|
|
@ -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_
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,10 @@
|
|||
#include <set>
|
||||
|
||||
#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<SegmentInfo>& segment_infos,
|
||||
XmlNode* segment_timeline) {
|
||||
for (std::list<SegmentInfo>::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<SegmentInfo>& 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(
|
||||
|
|
|
@ -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<SegmentInfo>& segment_infos);
|
||||
|
||||
private:
|
||||
// Add AudioChannelConfiguration elements. This will add multiple
|
||||
// AudioChannelConfiguration if @a repeated_audio_info contains multiple
|
||||
|
|
|
@ -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 <list>
|
||||
|
||||
#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<xmlDoc>::type MakeDoc(ScopedXmlPtr<xmlNode>::type node) {
|
||||
xml::ScopedXmlPtr<xmlDoc>::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<xmlDoc>::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
|
||||
// <?xml version="" encoding="UTF-8"?>
|
||||
// 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<SegmentInfo> 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[] =
|
||||
"<A>\n"
|
||||
" <B\n"
|
||||
|
@ -120,7 +165,7 @@ TEST(MetaTest, XmlEqual) {
|
|||
ASSERT_FALSE(XmlEqual(kXml1AttributeReorder, kXml1ChildrenReordered));
|
||||
}
|
||||
|
||||
TEST(Representation, AddContentProtectionXml) {
|
||||
TEST_F(RepresentationTest, AddContentProtectionXml) {
|
||||
static const char kExpectedRepresentaionString[] =
|
||||
"<Representation>\n"
|
||||
" <ContentProtection\n"
|
||||
|
@ -146,14 +191,30 @@ TEST(Representation, AddContentProtectionXml) {
|
|||
AddAttribute("c", "3", subelement);
|
||||
AddAttribute("d", "4", subelement);
|
||||
|
||||
RepresentationXmlNode representation;
|
||||
ASSERT_TRUE(
|
||||
representation.AddContentProtectionElementsFromMediaInfo(media_info));
|
||||
|
||||
ScopedXmlPtr<xmlDoc>::type doc(MakeDoc(representation.PassScopedPtr()));
|
||||
representation_.AddContentProtectionElementsFromMediaInfo(media_info));
|
||||
ScopedXmlPtr<xmlDoc>::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
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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">
|
||||
<Period start="PT0S">
|
||||
<AdaptationSet id="0">
|
||||
<Representation id="0" bandwidth="102400" codecs="avc1.010101" mimeType="video/mp4" width="720" height="480">
|
||||
<SegmentTemplate timescale="1000" initialization="init.mp4" media="$Time$.mp4">
|
||||
<SegmentTimeline>
|
||||
<S t="0" d="10"/>
|
||||
</SegmentTimeline>
|
||||
</SegmentTemplate>
|
||||
</Representation>
|
||||
</AdaptationSet>
|
||||
</Period>
|
||||
</MPD>
|
|
@ -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);
|
||||
|
|
|
@ -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<std::string>::const_iterator it = base_urls_.begin();
|
||||
it != base_urls_.end();
|
||||
++it) {
|
||||
|
|
Loading…
Reference in New Issue