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:
Rintaro Kuroiwa 2014-05-21 19:16:17 -07:00
parent 5b1980651f
commit 4a0193a816
17 changed files with 969 additions and 73 deletions

View File

@ -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_));
}

View File

@ -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_

View File

@ -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

View File

@ -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());
}

View File

@ -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);
};

View File

@ -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

View File

@ -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";
}

View File

@ -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);

26
mpd/base/segment_info.h Normal file
View File

@ -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_

View File

@ -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();
}

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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>

View File

@ -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);

View File

@ -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) {