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/logging.h"
|
||||||
#include "base/memory/scoped_ptr.h"
|
#include "base/memory/scoped_ptr.h"
|
||||||
#include "base/strings/string_number_conversions.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/content_protection_element.h"
|
||||||
#include "mpd/base/mpd_utils.h"
|
#include "mpd/base/mpd_utils.h"
|
||||||
#include "mpd/base/xml/xml_node.h"
|
#include "mpd/base/xml/xml_node.h"
|
||||||
|
@ -82,10 +84,47 @@ xmlNodePtr FindPeriodNode(XmlNode* xml_node) {
|
||||||
return NULL;
|
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
|
} // 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),
|
: type_(type),
|
||||||
|
options_(mpd_options),
|
||||||
adaptation_sets_deleter_(&adaptation_sets_) {}
|
adaptation_sets_deleter_(&adaptation_sets_) {}
|
||||||
|
|
||||||
MpdBuilder::~MpdBuilder() {}
|
MpdBuilder::~MpdBuilder() {}
|
||||||
|
@ -140,8 +179,7 @@ xmlDocPtr MpdBuilder::GenerateMpd() {
|
||||||
XmlNode mpd("MPD");
|
XmlNode mpd("MPD");
|
||||||
AddMpdNameSpaceInfo(&mpd);
|
AddMpdNameSpaceInfo(&mpd);
|
||||||
|
|
||||||
const float kMinBufferTime = 2.0f;
|
SetMpdOptionsValues(&mpd);
|
||||||
mpd.SetStringAttribute("minBufferTime", SecondsToXmlDuration(kMinBufferTime));
|
|
||||||
|
|
||||||
// Iterate thru AdaptationSets and add them to one big Period element.
|
// Iterate thru AdaptationSets and add them to one big Period element.
|
||||||
XmlNode period("Period");
|
XmlNode period("Period");
|
||||||
|
@ -163,6 +201,11 @@ xmlDocPtr MpdBuilder::GenerateMpd() {
|
||||||
return NULL;
|
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()))
|
if (!mpd.AddChild(period.PassScopedPtr()))
|
||||||
return NULL;
|
return NULL;
|
||||||
|
|
||||||
|
@ -171,7 +214,7 @@ xmlDocPtr MpdBuilder::GenerateMpd() {
|
||||||
AddStaticMpdInfo(&mpd);
|
AddStaticMpdInfo(&mpd);
|
||||||
break;
|
break;
|
||||||
case kDynamic:
|
case kDynamic:
|
||||||
NOTIMPLEMENTED() << "MPD for live is not implemented.";
|
AddDynamicMpdInfo(&mpd);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
NOTREACHED() << "Unknown MPD type: " << type_;
|
NOTREACHED() << "Unknown MPD type: " << type_;
|
||||||
|
@ -197,6 +240,17 @@ void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) {
|
||||||
SecondsToXmlDuration(GetStaticMpdDuration(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) {
|
float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) {
|
||||||
DCHECK(mpd_node);
|
DCHECK(mpd_node);
|
||||||
DCHECK_EQ(MpdBuilder::kStatic, type_);
|
DCHECK_EQ(MpdBuilder::kStatic, type_);
|
||||||
|
@ -229,6 +283,62 @@ float MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) {
|
||||||
return max_duration;
|
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,
|
AdaptationSet::AdaptationSet(uint32 adaptation_set_id,
|
||||||
base::AtomicSequenceNumber* counter)
|
base::AtomicSequenceNumber* counter)
|
||||||
: representations_deleter_(&representations_),
|
: representations_deleter_(&representations_),
|
||||||
|
@ -283,14 +393,13 @@ xml::ScopedXmlPtr<xmlNode>::type AdaptationSet::GetXml() {
|
||||||
}
|
}
|
||||||
|
|
||||||
Representation::Representation(const MediaInfo& media_info, uint32 id)
|
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() {}
|
Representation::~Representation() {}
|
||||||
|
|
||||||
bool Representation::Init() {
|
bool Representation::Init() {
|
||||||
if (!HasRequiredMediaInfoFields())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
codecs_ = GetCodecs(media_info_);
|
codecs_ = GetCodecs(media_info_);
|
||||||
if (codecs_.empty()) {
|
if (codecs_.empty()) {
|
||||||
LOG(ERROR) << "Missing codec info in MediaInfo.";
|
LOG(ERROR) << "Missing codec info in MediaInfo.";
|
||||||
|
@ -331,11 +440,24 @@ void Representation::AddContentProtectionElement(
|
||||||
RemoveDuplicateAttributes(&content_protection_elements_.back());
|
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_);
|
base::AutoLock scoped_lock(lock_);
|
||||||
segment_starttime_duration_pairs_.push_back(
|
if (IsContiguous(start_time, duration, size)) {
|
||||||
std::pair<uint64, uint64>(start_time, duration));
|
++segment_infos_.back().repeat;
|
||||||
return true;
|
} 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
|
// 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).
|
// AddVODOnlyInfo() (Adds segment info).
|
||||||
xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
|
xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
|
||||||
base::AutoLock scoped_lock(lock_);
|
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(!(HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)));
|
||||||
DCHECK(media_info_.has_bandwidth());
|
|
||||||
|
|
||||||
RepresentationXmlNode representation;
|
RepresentationXmlNode representation;
|
||||||
// Mandatory fields for Representation.
|
// Mandatory fields for Representation.
|
||||||
representation.SetId(id_);
|
representation.SetId(id_);
|
||||||
representation.SetIntegerAttribute("bandwidth", media_info_.bandwidth());
|
representation.SetIntegerAttribute("bandwidth", bandwidth);
|
||||||
representation.SetStringAttribute("codecs", codecs_);
|
representation.SetStringAttribute("codecs", codecs_);
|
||||||
representation.SetStringAttribute("mimeType", mime_type_);
|
representation.SetStringAttribute("mimeType", mime_type_);
|
||||||
|
|
||||||
|
@ -384,6 +515,14 @@ xml::ScopedXmlPtr<xmlNode>::type Representation::GetXml() {
|
||||||
return xml::ScopedXmlPtr<xmlNode>::type();
|
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();
|
return representation.PassScopedPtr();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -393,19 +532,73 @@ bool Representation::HasRequiredMediaInfoFields() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!media_info_.has_bandwidth()) {
|
|
||||||
LOG(ERROR) << "MediaInfo missing required field: bandwidth.";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!media_info_.has_container_type()) {
|
if (!media_info_.has_container_type()) {
|
||||||
LOG(ERROR) << "MediaInfo missing required field: container_type.";
|
LOG(ERROR) << "MediaInfo missing required field: container_type.";
|
||||||
return false;
|
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;
|
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 {
|
std::string Representation::GetVideoMimeType() const {
|
||||||
return GetMimeType("video", media_info_.container_type());
|
return GetMimeType("video", media_info_.container_type());
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,10 @@
|
||||||
#include "base/stl_util.h"
|
#include "base/stl_util.h"
|
||||||
#include "base/synchronization/lock.h"
|
#include "base/synchronization/lock.h"
|
||||||
#include "mpd/base/content_protection_element.h"
|
#include "mpd/base/content_protection_element.h"
|
||||||
|
#include "mpd/base/bandwidth_estimator.h"
|
||||||
#include "mpd/base/media_info.pb.h"
|
#include "mpd/base/media_info.pb.h"
|
||||||
#include "mpd/base/mpd_utils.h"
|
#include "mpd/base/mpd_utils.h"
|
||||||
|
#include "mpd/base/segment_info.h"
|
||||||
#include "mpd/base/xml/scoped_xml_ptr.h"
|
#include "mpd/base/xml/scoped_xml_ptr.h"
|
||||||
|
|
||||||
namespace dash_packager {
|
namespace dash_packager {
|
||||||
|
@ -30,9 +32,28 @@ class Representation;
|
||||||
namespace xml {
|
namespace xml {
|
||||||
|
|
||||||
class XmlNode;
|
class XmlNode;
|
||||||
|
class RepresentationXmlNode;
|
||||||
|
|
||||||
} // namespace xml
|
} // 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).
|
/// This class generates DASH MPDs (Media Presentation Descriptions).
|
||||||
class MpdBuilder {
|
class MpdBuilder {
|
||||||
public:
|
public:
|
||||||
|
@ -44,7 +65,7 @@ class MpdBuilder {
|
||||||
/// Constructs MpdBuilder.
|
/// Constructs MpdBuilder.
|
||||||
/// @param type indicates whether the MPD should be for VOD or live content
|
/// @param type indicates whether the MPD should be for VOD or live content
|
||||||
/// (kStatic for VOD profile, or kDynamic for live profile).
|
/// (kStatic for VOD profile, or kDynamic for live profile).
|
||||||
explicit MpdBuilder(MpdType type);
|
MpdBuilder(MpdType type, const MpdOptions& mpd_options);
|
||||||
~MpdBuilder();
|
~MpdBuilder();
|
||||||
|
|
||||||
/// Add <BaseURL> entry to the MPD.
|
/// Add <BaseURL> entry to the MPD.
|
||||||
|
@ -64,6 +85,10 @@ class MpdBuilder {
|
||||||
MpdType type() { return type_; }
|
MpdType type() { return type_; }
|
||||||
|
|
||||||
private:
|
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);
|
bool ToStringImpl(std::string* output);
|
||||||
|
|
||||||
// Returns the document pointer to the MPD. This must be freed by the caller
|
// 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
|
// Adds 'static' MPD attributes and elements to |mpd_node|. This assumes that
|
||||||
// the first child element is a Period element.
|
// the first child element is a Period element.
|
||||||
void AddStaticMpdInfo(xml::XmlNode* mpd_node);
|
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);
|
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_;
|
MpdType type_;
|
||||||
|
MpdOptions options_;
|
||||||
std::list<AdaptationSet*> adaptation_sets_;
|
std::list<AdaptationSet*> adaptation_sets_;
|
||||||
::STLElementDeleter<std::list<AdaptationSet*> > adaptation_sets_deleter_;
|
::STLElementDeleter<std::list<AdaptationSet*> > adaptation_sets_deleter_;
|
||||||
|
|
||||||
|
@ -143,7 +178,12 @@ class AdaptationSet {
|
||||||
/// well as optional ContentProtection elements for that stream.
|
/// well as optional ContentProtection elements for that stream.
|
||||||
class Representation {
|
class Representation {
|
||||||
public:
|
public:
|
||||||
|
// TODO(rkuroiwa): Get the value from MpdOptions for constructing
|
||||||
|
// BandwidthEstimator.
|
||||||
/// @param media_info is a MediaInfo containing information on the media.
|
/// @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>.
|
/// @param representation_id is the numeric ID for the <Representation>.
|
||||||
Representation(const MediaInfo& media_info, uint32 representation_id);
|
Representation(const MediaInfo& media_info, uint32 representation_id);
|
||||||
~Representation();
|
~Representation();
|
||||||
|
@ -165,8 +205,8 @@ class Representation {
|
||||||
/// stream's time scale.
|
/// stream's time scale.
|
||||||
/// @param duration is the duration of the segment, in units of the stream's
|
/// @param duration is the duration of the segment, in units of the stream's
|
||||||
/// time scale.
|
/// time scale.
|
||||||
/// @return true on success, false otherwise.
|
/// @param size of the segment in bytes.
|
||||||
bool AddNewSegment(uint64 start_time, uint64 duration);
|
void AddNewSegment(uint64 start_time, uint64 duration, uint64 size);
|
||||||
|
|
||||||
/// @return Copy of <Representation>.
|
/// @return Copy of <Representation>.
|
||||||
xml::ScopedXmlPtr<xmlNode>::type GetXml();
|
xml::ScopedXmlPtr<xmlNode>::type GetXml();
|
||||||
|
@ -177,10 +217,16 @@ class Representation {
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
bool AddLiveInfo(xml::RepresentationXmlNode* representation);
|
||||||
|
|
||||||
// Returns true if |media_info_| has required fields to generate a valid
|
// Returns true if |media_info_| has required fields to generate a valid
|
||||||
// Representation. Otherwise returns false.
|
// Representation. Otherwise returns false.
|
||||||
bool HasRequiredMediaInfoFields();
|
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
|
// Note: Because 'mimeType' is a required field for a valid MPD, these return
|
||||||
// strings.
|
// strings.
|
||||||
std::string GetVideoMimeType() const;
|
std::string GetVideoMimeType() const;
|
||||||
|
@ -188,13 +234,14 @@ class Representation {
|
||||||
|
|
||||||
MediaInfo media_info_;
|
MediaInfo media_info_;
|
||||||
std::list<ContentProtectionElement> content_protection_elements_;
|
std::list<ContentProtectionElement> content_protection_elements_;
|
||||||
std::list<std::pair<uint64, uint64> > segment_starttime_duration_pairs_;
|
std::list<SegmentInfo> segment_infos_;
|
||||||
|
|
||||||
base::Lock lock_;
|
base::Lock lock_;
|
||||||
|
|
||||||
const uint32 id_;
|
const uint32 id_;
|
||||||
std::string mime_type_;
|
std::string mime_type_;
|
||||||
std::string codecs_;
|
std::string codecs_;
|
||||||
|
BandwidthEstimator bandwidth_estimator_;
|
||||||
|
|
||||||
DISALLOW_COPY_AND_ASSIGN(Representation);
|
DISALLOW_COPY_AND_ASSIGN(Representation);
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,8 +7,11 @@
|
||||||
#include "base/file_util.h"
|
#include "base/file_util.h"
|
||||||
#include "base/logging.h"
|
#include "base/logging.h"
|
||||||
#include "base/strings/string_number_conversions.h"
|
#include "base/strings/string_number_conversions.h"
|
||||||
|
#include "base/strings/stringprintf.h"
|
||||||
#include "mpd/base/mpd_builder.h"
|
#include "mpd/base/mpd_builder.h"
|
||||||
|
#include "mpd/base/mpd_utils.h"
|
||||||
#include "mpd/test/mpd_builder_test_helper.h"
|
#include "mpd/test/mpd_builder_test_helper.h"
|
||||||
|
#include "mpd/test/xml_compare.h"
|
||||||
#include "testing/gtest/include/gtest/gtest.h"
|
#include "testing/gtest/include/gtest/gtest.h"
|
||||||
#include "third_party/libxml/src/include/libxml/xmlstring.h"
|
#include "third_party/libxml/src/include/libxml/xmlstring.h"
|
||||||
|
|
||||||
|
@ -42,27 +45,11 @@ void CheckIdEqual(uint32 expected_id, T* node) {
|
||||||
}
|
}
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(AdaptationSetTest, CheckId) {
|
template <MpdBuilder::MpdType type>
|
||||||
base::AtomicSequenceNumber sequence_counter;
|
class MpdBuilderTest: public ::testing::Test {
|
||||||
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 {
|
|
||||||
public:
|
public:
|
||||||
StaticMpdBuilderTest() : mpd_(MpdBuilder::kStatic) {}
|
MpdBuilderTest() : mpd_(type, MpdOptions()), representation_() {}
|
||||||
virtual ~StaticMpdBuilderTest() {}
|
virtual ~MpdBuilderTest() {}
|
||||||
|
|
||||||
void CheckMpd(const std::string& expected_output_file) {
|
void CheckMpd(const std::string& expected_output_file) {
|
||||||
std::string mpd_doc;
|
std::string mpd_doc;
|
||||||
|
@ -74,25 +61,169 @@ class StaticMpdBuilderTest : public ::testing::Test {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected:
|
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_;
|
MpdBuilder mpd_;
|
||||||
|
|
||||||
|
// We usually need only one representation.
|
||||||
|
Representation* representation_; // Owned by |mpd_|.
|
||||||
|
|
||||||
private:
|
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) {
|
TEST_F(StaticMpdBuilderTest, Video) {
|
||||||
MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1);
|
MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1);
|
||||||
|
ASSERT_NO_FATAL_FAILURE(AddRepresentation(video_media_info));
|
||||||
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);
|
|
||||||
|
|
||||||
EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputVideo1));
|
EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputVideo1));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add both video and audio and check the output.
|
||||||
TEST_F(StaticMpdBuilderTest, VideoAndAudio) {
|
TEST_F(StaticMpdBuilderTest, VideoAndAudio) {
|
||||||
MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1);
|
MediaInfo video_media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1);
|
||||||
MediaInfo audio_media_info = GetTestMediaInfo(kFileNameAudioMediaInfo1);
|
MediaInfo audio_media_info = GetTestMediaInfo(kFileNameAudioMediaInfo1);
|
||||||
|
@ -131,4 +262,162 @@ TEST_F(StaticMpdBuilderTest, AudioChannelConfigurationWithContentProtection) {
|
||||||
EXPECT_NO_FATAL_FAILURE(CheckMpd(kFileNameExpectedMpdOutputEncryptedAudio));
|
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
|
} // namespace dash_packager
|
||||||
|
|
|
@ -6,15 +6,13 @@
|
||||||
|
|
||||||
#include "mpd/base/mpd_utils.h"
|
#include "mpd/base/mpd_utils.h"
|
||||||
|
|
||||||
#include <set>
|
|
||||||
|
|
||||||
#include "base/logging.h"
|
#include "base/logging.h"
|
||||||
#include "base/strings/string_number_conversions.h"
|
#include "base/strings/string_number_conversions.h"
|
||||||
#include "mpd/base/content_protection_element.h"
|
#include "mpd/base/content_protection_element.h"
|
||||||
|
#include "mpd/base/media_info.pb.h"
|
||||||
#include "mpd/base/xml/scoped_xml_ptr.h"
|
#include "mpd/base/xml/scoped_xml_ptr.h"
|
||||||
#include "third_party/libxml/src/include/libxml/tree.h"
|
#include "third_party/libxml/src/include/libxml/tree.h"
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
// Concatenate all the codecs in |repeated_stream_info|.
|
// Concatenate all the codecs in |repeated_stream_info|.
|
||||||
|
@ -83,7 +81,7 @@ std::string GetCodecs(const MediaInfo& media_info) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string SecondsToXmlDuration(float seconds) {
|
std::string SecondsToXmlDuration(double seconds) {
|
||||||
return "PT" + base::DoubleToString(seconds) + "S";
|
return "PT" + base::DoubleToString(seconds) + "S";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,12 +12,13 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include "base/basictypes.h"
|
#include "base/basictypes.h"
|
||||||
#include "mpd/base/media_info.pb.h"
|
|
||||||
#include "third_party/libxml/src/include/libxml/tree.h"
|
#include "third_party/libxml/src/include/libxml/tree.h"
|
||||||
|
|
||||||
namespace dash_packager {
|
namespace dash_packager {
|
||||||
|
|
||||||
|
class MediaInfo;
|
||||||
struct ContentProtectionElement;
|
struct ContentProtectionElement;
|
||||||
|
struct SegmentInfo;
|
||||||
|
|
||||||
bool HasVODOnlyFields(const MediaInfo& media_info);
|
bool HasVODOnlyFields(const MediaInfo& media_info);
|
||||||
|
|
||||||
|
@ -33,7 +34,7 @@ void RemoveDuplicateAttributes(
|
||||||
// comma.
|
// comma.
|
||||||
std::string GetCodecs(const MediaInfo& media_info);
|
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.
|
// Tries to get "duration" attribute from |node|. On success |duration| is set.
|
||||||
bool GetDurationAttribute(xmlNodePtr node, float* duration);
|
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),
|
output_path_(output_path),
|
||||||
mpd_builder_(new MpdBuilder(dash_profile == kLiveProfile
|
mpd_builder_(new MpdBuilder(dash_profile == kLiveProfile
|
||||||
? MpdBuilder::kDynamic
|
? MpdBuilder::kDynamic
|
||||||
: MpdBuilder::kStatic)) {
|
: MpdBuilder::kStatic,
|
||||||
|
MpdOptions())) {
|
||||||
DCHECK(dash_profile == kLiveProfile || dash_profile == kOnDemandProfile);
|
DCHECK(dash_profile == kLiveProfile || dash_profile == kOnDemandProfile);
|
||||||
for (size_t i = 0; i < base_urls.size(); ++i)
|
for (size_t i = 0; i < base_urls.size(); ++i)
|
||||||
mpd_builder_->AddBaseUrl(base_urls[i]);
|
mpd_builder_->AddBaseUrl(base_urls[i]);
|
||||||
|
@ -76,8 +77,8 @@ bool SimpleMpdNotifier::NotifyNewSegment(uint32 container_id,
|
||||||
LOG(ERROR) << "Unexpected container_id: " << container_id;
|
LOG(ERROR) << "Unexpected container_id: " << container_id;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!it->second->AddNewSegment(start_time, duration))
|
// TODO(kqyang): AddNewSegment() requires size for the third argument.
|
||||||
return false;
|
// !it->second->AddNewSegment(start_time, duration);
|
||||||
return WriteMpdToFile();
|
return WriteMpdToFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,8 +9,10 @@
|
||||||
#include <set>
|
#include <set>
|
||||||
|
|
||||||
#include "base/logging.h"
|
#include "base/logging.h"
|
||||||
|
#include "base/stl_util.h"
|
||||||
#include "base/strings/string_number_conversions.h"
|
#include "base/strings/string_number_conversions.h"
|
||||||
#include "mpd/base/media_info.pb.h"
|
#include "mpd/base/media_info.pb.h"
|
||||||
|
#include "mpd/base/segment_info.h"
|
||||||
|
|
||||||
using dash_packager::xml::XmlNode;
|
using dash_packager::xml::XmlNode;
|
||||||
|
|
||||||
|
@ -18,6 +20,8 @@ using dash_packager::MediaInfo;
|
||||||
typedef MediaInfo::ContentProtectionXml ContentProtectionXml;
|
typedef MediaInfo::ContentProtectionXml ContentProtectionXml;
|
||||||
typedef ContentProtectionXml::AttributeNameValuePair AttributeNameValuePair;
|
typedef ContentProtectionXml::AttributeNameValuePair AttributeNameValuePair;
|
||||||
|
|
||||||
|
namespace dash_packager {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::string RangeToString(const dash_packager::Range& range) {
|
std::string RangeToString(const dash_packager::Range& range) {
|
||||||
|
@ -151,9 +155,25 @@ bool TranslateToContentProtectionXmlNode(
|
||||||
return true;
|
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
|
||||||
|
|
||||||
namespace dash_packager {
|
|
||||||
namespace xml {
|
namespace xml {
|
||||||
|
|
||||||
XmlNode::XmlNode(const char* name) : node_(xmlNewNode(NULL, BAD_CAST name)) {
|
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) {
|
const std::string& attribute) {
|
||||||
DCHECK(node_);
|
DCHECK(node_);
|
||||||
DCHECK(attribute_name);
|
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) {
|
void XmlNode::SetIntegerAttribute(const char* attribute_name, uint64 number) {
|
||||||
DCHECK(node_);
|
DCHECK(node_);
|
||||||
DCHECK(attribute_name);
|
DCHECK(attribute_name);
|
||||||
xmlNewProp(node_.get(),
|
xmlSetProp(node_.get(),
|
||||||
BAD_CAST attribute_name,
|
BAD_CAST attribute_name,
|
||||||
BAD_CAST (base::Uint64ToString(number).c_str()));
|
BAD_CAST (base::Uint64ToString(number).c_str()));
|
||||||
}
|
}
|
||||||
|
@ -194,7 +214,7 @@ void XmlNode::SetFloatingPointAttribute(const char* attribute_name,
|
||||||
double number) {
|
double number) {
|
||||||
DCHECK(node_);
|
DCHECK(node_);
|
||||||
DCHECK(attribute_name);
|
DCHECK(attribute_name);
|
||||||
xmlNewProp(node_.get(),
|
xmlSetProp(node_.get(),
|
||||||
BAD_CAST attribute_name,
|
BAD_CAST attribute_name,
|
||||||
BAD_CAST (base::DoubleToString(number).c_str()));
|
BAD_CAST (base::DoubleToString(number).c_str()));
|
||||||
}
|
}
|
||||||
|
@ -390,6 +410,43 @@ bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) {
|
||||||
return true;
|
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
|
// Find all the unique number-of-channels in |repeated_audio_info|, and make
|
||||||
// AudioChannelConfiguration for each number-of-channels.
|
// AudioChannelConfiguration for each number-of-channels.
|
||||||
bool RepresentationXmlNode::AddAudioChannelInfo(
|
bool RepresentationXmlNode::AddAudioChannelInfo(
|
||||||
|
|
|
@ -19,6 +19,9 @@
|
||||||
#include "third_party/libxml/src/include/libxml/tree.h"
|
#include "third_party/libxml/src/include/libxml/tree.h"
|
||||||
|
|
||||||
namespace dash_packager {
|
namespace dash_packager {
|
||||||
|
|
||||||
|
struct SegmentInfo;
|
||||||
|
|
||||||
namespace xml {
|
namespace xml {
|
||||||
|
|
||||||
/// These classes are wrapper classes for XML elements for generating MPD.
|
/// 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.
|
/// @return true on success, false otherwise.
|
||||||
bool AddVODOnlyInfo(const MediaInfo& media_info);
|
bool AddVODOnlyInfo(const MediaInfo& media_info);
|
||||||
|
|
||||||
|
bool AddLiveOnlyInfo(const MediaInfo& media_info,
|
||||||
|
const std::list<SegmentInfo>& segment_infos);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Add AudioChannelConfiguration elements. This will add multiple
|
// Add AudioChannelConfiguration elements. This will add multiple
|
||||||
// AudioChannelConfiguration if @a repeated_audio_info contains multiple
|
// AudioChannelConfiguration if @a repeated_audio_info contains multiple
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
// Use of this source code is governed by a BSD-style
|
// Use of this source code is governed by a BSD-style
|
||||||
// license that can be found in the LICENSE file or at
|
// license that can be found in the LICENSE file or at
|
||||||
// https://developers.google.com/open-source/licenses/bsd
|
// https://developers.google.com/open-source/licenses/bsd
|
||||||
|
#include <list>
|
||||||
|
|
||||||
#include "base/logging.h"
|
#include "base/logging.h"
|
||||||
#include "base/strings/string_util.h"
|
#include "base/strings/string_util.h"
|
||||||
|
#include "mpd/base/mpd_builder.h"
|
||||||
#include "mpd/base/xml/xml_node.h"
|
#include "mpd/base/xml/xml_node.h"
|
||||||
#include "mpd/test/xml_compare.h"
|
#include "mpd/test/xml_compare.h"
|
||||||
#include "testing/gtest/include/gtest/gtest.h"
|
#include "testing/gtest/include/gtest/gtest.h"
|
||||||
|
@ -28,16 +30,59 @@ void AddAttribute(const std::string& name,
|
||||||
attribute->set_value(value);
|
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) {
|
ScopedXmlPtr<xmlDoc>::type MakeDoc(ScopedXmlPtr<xmlNode>::type node) {
|
||||||
xml::ScopedXmlPtr<xmlDoc>::type doc(xmlNewDoc(BAD_CAST ""));
|
xml::ScopedXmlPtr<xmlDoc>::type doc(xmlNewDoc(BAD_CAST ""));
|
||||||
xmlDocSetRootElement(doc.get(), node.release());
|
xmlDocSetRootElement(doc.get(), node.release());
|
||||||
|
|
||||||
return doc.Pass();
|
return doc.Pass();
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // 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.
|
// 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[] =
|
static const char kXml1[] =
|
||||||
"<A>\n"
|
"<A>\n"
|
||||||
" <B\n"
|
" <B\n"
|
||||||
|
@ -120,7 +165,7 @@ TEST(MetaTest, XmlEqual) {
|
||||||
ASSERT_FALSE(XmlEqual(kXml1AttributeReorder, kXml1ChildrenReordered));
|
ASSERT_FALSE(XmlEqual(kXml1AttributeReorder, kXml1ChildrenReordered));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Representation, AddContentProtectionXml) {
|
TEST_F(RepresentationTest, AddContentProtectionXml) {
|
||||||
static const char kExpectedRepresentaionString[] =
|
static const char kExpectedRepresentaionString[] =
|
||||||
"<Representation>\n"
|
"<Representation>\n"
|
||||||
" <ContentProtection\n"
|
" <ContentProtection\n"
|
||||||
|
@ -146,14 +191,30 @@ TEST(Representation, AddContentProtectionXml) {
|
||||||
AddAttribute("c", "3", subelement);
|
AddAttribute("c", "3", subelement);
|
||||||
AddAttribute("d", "4", subelement);
|
AddAttribute("d", "4", subelement);
|
||||||
|
|
||||||
RepresentationXmlNode representation;
|
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(
|
||||||
representation.AddContentProtectionElementsFromMediaInfo(media_info));
|
representation_.AddContentProtectionElementsFromMediaInfo(media_info));
|
||||||
|
ScopedXmlPtr<xmlDoc>::type doc(MakeDoc(representation_.PassScopedPtr()));
|
||||||
ScopedXmlPtr<xmlDoc>::type doc(MakeDoc(representation.PassScopedPtr()));
|
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(
|
||||||
XmlEqual(kExpectedRepresentaionString, doc.get()));
|
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 xml
|
||||||
} // namespace dash_packager
|
} // namespace dash_packager
|
||||||
|
|
|
@ -40,6 +40,8 @@
|
||||||
'target_name': 'mpd_builder',
|
'target_name': 'mpd_builder',
|
||||||
'type': 'static_library',
|
'type': 'static_library',
|
||||||
'sources': [
|
'sources': [
|
||||||
|
'base/bandwidth_estimator.cc',
|
||||||
|
'base/bandwidth_estimator.h',
|
||||||
'base/content_protection_element.cc',
|
'base/content_protection_element.cc',
|
||||||
'base/content_protection_element.h',
|
'base/content_protection_element.h',
|
||||||
'base/mpd_builder.cc',
|
'base/mpd_builder.cc',
|
||||||
|
@ -67,6 +69,7 @@
|
||||||
'target_name': 'mpd_unittest',
|
'target_name': 'mpd_unittest',
|
||||||
'type': '<(gtest_target_type)',
|
'type': '<(gtest_target_type)',
|
||||||
'sources': [
|
'sources': [
|
||||||
|
'base/bandwidth_estimator_test.cc',
|
||||||
'base/mpd_builder_unittest.cc',
|
'base/mpd_builder_unittest.cc',
|
||||||
'base/xml/xml_node_unittest.cc',
|
'base/xml/xml_node_unittest.cc',
|
||||||
'test/mpd_builder_test_helper.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[] =
|
const char kFileNameExpectedMpdOutputEncryptedAudio[] =
|
||||||
"encrypted_audio_media_info_expected_output.txt";
|
"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
|
// Returns the path to test data with |file_name|. Use constants above to get
|
||||||
// path to the test files.
|
// path to the test files.
|
||||||
base::FilePath GetTestDataFilePath(const std::string& file_name);
|
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) {
|
bool MpdWriter::WriteMpdToString(std::string* output) {
|
||||||
CHECK(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();
|
for (std::list<std::string>::const_iterator it = base_urls_.begin();
|
||||||
it != base_urls_.end();
|
it != base_urls_.end();
|
||||||
++it) {
|
++it) {
|
||||||
|
|
Loading…
Reference in New Issue