Shaka Packager SDK
mpd_builder.cc
1 // Copyright 2014 Google Inc. All rights reserved.
2 //
3 // Use of this source code is governed by a BSD-style
4 // license that can be found in the LICENSE file or at
5 // https://developers.google.com/open-source/licenses/bsd
6 
7 #include "packager/mpd/base/mpd_builder.h"
8 
9 #include <algorithm>
10 
11 #include "packager/base/files/file_path.h"
12 #include "packager/base/logging.h"
13 #include "packager/base/strings/string_number_conversions.h"
14 #include "packager/base/strings/stringprintf.h"
15 #include "packager/base/synchronization/lock.h"
16 #include "packager/base/time/default_clock.h"
17 #include "packager/base/time/time.h"
18 #include "packager/mpd/base/adaptation_set.h"
19 #include "packager/mpd/base/mpd_utils.h"
20 #include "packager/mpd/base/period.h"
21 #include "packager/mpd/base/representation.h"
22 #include "packager/mpd/base/xml/xml_node.h"
23 #include "packager/version/version.h"
24 
25 namespace shaka {
26 
27 using base::FilePath;
28 using xml::XmlNode;
29 
30 namespace {
31 
32 void AddMpdNameSpaceInfo(XmlNode* mpd) {
33  DCHECK(mpd);
34 
35  static const char kXmlNamespace[] = "urn:mpeg:dash:schema:mpd:2011";
36  static const char kXmlNamespaceXsi[] =
37  "http://www.w3.org/2001/XMLSchema-instance";
38  static const char kXmlNamespaceXlink[] = "http://www.w3.org/1999/xlink";
39  static const char kDashSchemaMpd2011[] =
40  "urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd";
41  static const char kCencNamespace[] = "urn:mpeg:cenc:2013";
42 
43  mpd->SetStringAttribute("xmlns", kXmlNamespace);
44  mpd->SetStringAttribute("xmlns:xsi", kXmlNamespaceXsi);
45  mpd->SetStringAttribute("xmlns:xlink", kXmlNamespaceXlink);
46  mpd->SetStringAttribute("xsi:schemaLocation", kDashSchemaMpd2011);
47  mpd->SetStringAttribute("xmlns:cenc", kCencNamespace);
48 }
49 
50 bool Positive(double d) {
51  return d > 0.0;
52 }
53 
54 // Return current time in XML DateTime format. The value is in UTC, so the
55 // string ends with a 'Z'.
56 std::string XmlDateTimeNowWithOffset(
57  int32_t offset_seconds,
58  base::Clock* clock) {
59  base::Time time = clock->Now();
60  time += base::TimeDelta::FromSeconds(offset_seconds);
61  base::Time::Exploded time_exploded;
62  time.UTCExplode(&time_exploded);
63 
64  return base::StringPrintf("%4d-%02d-%02dT%02d:%02d:%02dZ", time_exploded.year,
65  time_exploded.month, time_exploded.day_of_month,
66  time_exploded.hour, time_exploded.minute,
67  time_exploded.second);
68 }
69 
70 void SetIfPositive(const char* attr_name, double value, XmlNode* mpd) {
71  if (Positive(value)) {
72  mpd->SetStringAttribute(attr_name, SecondsToXmlDuration(value));
73  }
74 }
75 
76 std::string MakePathRelative(const std::string& media_path,
77  const FilePath& parent_path) {
78  FilePath relative_path;
79  const FilePath child_path = FilePath::FromUTF8Unsafe(media_path);
80  const bool is_child =
81  parent_path.AppendRelativePath(child_path, &relative_path);
82  if (!is_child)
83  relative_path = child_path;
84  return relative_path.NormalizePathSeparatorsTo('/').AsUTF8Unsafe();
85 }
86 
87 // Spooky static initialization/cleanup of libxml.
88 class LibXmlInitializer {
89  public:
90  LibXmlInitializer() : initialized_(false) {
91  base::AutoLock lock(lock_);
92  if (!initialized_) {
93  xmlInitParser();
94  initialized_ = true;
95  }
96  }
97 
98  ~LibXmlInitializer() {
99  base::AutoLock lock(lock_);
100  if (initialized_) {
101  xmlCleanupParser();
102  initialized_ = false;
103  }
104  }
105 
106  private:
107  base::Lock lock_;
108  bool initialized_;
109 
110  DISALLOW_COPY_AND_ASSIGN(LibXmlInitializer);
111 };
112 
113 } // namespace
114 
116  : mpd_options_(mpd_options), clock_(new base::DefaultClock()) {}
117 
118 MpdBuilder::~MpdBuilder() {}
119 
120 void MpdBuilder::AddBaseUrl(const std::string& base_url) {
121  base_urls_.push_back(base_url);
122 }
123 
124 Period* MpdBuilder::GetOrCreatePeriod(double start_time_in_seconds) {
125  for (auto& period : periods_) {
126  const double kPeriodTimeDriftThresholdInSeconds = 1.0;
127  const bool match =
128  std::fabs(period->start_time_in_seconds() - start_time_in_seconds) <
129  kPeriodTimeDriftThresholdInSeconds;
130  if (match)
131  return period.get();
132  }
133  periods_.emplace_back(
134  new Period(period_counter_.GetNext(), start_time_in_seconds, mpd_options_,
135  &adaptation_set_counter_, &representation_counter_));
136  return periods_.back().get();
137 }
138 
139 bool MpdBuilder::ToString(std::string* output) {
140  DCHECK(output);
141  static LibXmlInitializer lib_xml_initializer;
142 
143  xml::scoped_xml_ptr<xmlDoc> doc(GenerateMpd());
144  if (!doc)
145  return false;
146 
147  static const int kNiceFormat = 1;
148  int doc_str_size = 0;
149  xmlChar* doc_str = nullptr;
150  xmlDocDumpFormatMemoryEnc(doc.get(), &doc_str, &doc_str_size, "UTF-8",
151  kNiceFormat);
152  output->assign(doc_str, doc_str + doc_str_size);
153  xmlFree(doc_str);
154 
155  // Cleanup, free the doc.
156  doc.reset();
157  return true;
158 }
159 
160 xmlDocPtr MpdBuilder::GenerateMpd() {
161  // Setup nodes.
162  static const char kXmlVersion[] = "1.0";
163  xml::scoped_xml_ptr<xmlDoc> doc(xmlNewDoc(BAD_CAST kXmlVersion));
164  XmlNode mpd("MPD");
165 
166  // Add baseurls to MPD.
167  for (const std::string& base_url : base_urls_) {
168  XmlNode xml_base_url("BaseURL");
169  xml_base_url.SetContent(base_url);
170 
171  if (!mpd.AddChild(xml_base_url.PassScopedPtr()))
172  return nullptr;
173  }
174 
175  // Prefer Period@duration to Period@start for static MPD with more than one
176  // periods.
177  if (mpd_options_.mpd_type == MpdType::kStatic && periods_.size() > 1) {
178  // The duration of every period is determined by its start_time and next
179  // period start_time. The code below traverses |periods_| backwards.
180  double next_period_start_time = GetStaticMpdDuration();
181  std::for_each(
182  periods_.rbegin(), periods_.rend(),
183  [&next_period_start_time](const std::unique_ptr<Period>& period) {
184  period->set_duration_seconds(next_period_start_time -
185  period->start_time_in_seconds());
186  next_period_start_time = period->start_time_in_seconds();
187  });
188  }
189 
190  for (const auto& period : periods_) {
191  xml::scoped_xml_ptr<xmlNode> period_node(period->GetXml());
192  if (!period_node || !mpd.AddChild(std::move(period_node)))
193  return nullptr;
194  }
195 
196  AddMpdNameSpaceInfo(&mpd);
197 
198  static const char kOnDemandProfile[] =
199  "urn:mpeg:dash:profile:isoff-on-demand:2011";
200  static const char kLiveProfile[] =
201  "urn:mpeg:dash:profile:isoff-live:2011";
202  switch (mpd_options_.dash_profile) {
203  case DashProfile::kOnDemand:
204  mpd.SetStringAttribute("profiles", kOnDemandProfile);
205  break;
206  case DashProfile::kLive:
207  mpd.SetStringAttribute("profiles", kLiveProfile);
208  break;
209  default:
210  NOTREACHED() << "Unknown DASH profile: "
211  << static_cast<int>(mpd_options_.dash_profile);
212  break;
213  }
214 
215  AddCommonMpdInfo(&mpd);
216  switch (mpd_options_.mpd_type) {
217  case MpdType::kStatic:
218  AddStaticMpdInfo(&mpd);
219  break;
220  case MpdType::kDynamic:
221  AddDynamicMpdInfo(&mpd);
222  break;
223  default:
224  NOTREACHED() << "Unknown MPD type: "
225  << static_cast<int>(mpd_options_.mpd_type);
226  break;
227  }
228 
229  DCHECK(doc);
230  const std::string version = GetPackagerVersion();
231  if (!version.empty()) {
232  std::string version_string =
233  base::StringPrintf("Generated with %s version %s",
234  GetPackagerProjectUrl().c_str(), version.c_str());
235  xml::scoped_xml_ptr<xmlNode> comment(
236  xmlNewDocComment(doc.get(), BAD_CAST version_string.c_str()));
237  xmlDocSetRootElement(doc.get(), comment.get());
238  xmlAddSibling(comment.release(), mpd.Release());
239  } else {
240  xmlDocSetRootElement(doc.get(), mpd.Release());
241  }
242  return doc.release();
243 }
244 
245 void MpdBuilder::AddCommonMpdInfo(XmlNode* mpd_node) {
246  if (Positive(mpd_options_.mpd_params.min_buffer_time)) {
247  mpd_node->SetStringAttribute(
248  "minBufferTime",
249  SecondsToXmlDuration(mpd_options_.mpd_params.min_buffer_time));
250  } else {
251  LOG(ERROR) << "minBufferTime value not specified.";
252  // TODO(tinskip): Propagate error.
253  }
254 }
255 
256 void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) {
257  DCHECK(mpd_node);
258  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
259 
260  static const char kStaticMpdType[] = "static";
261  mpd_node->SetStringAttribute("type", kStaticMpdType);
262  mpd_node->SetStringAttribute("mediaPresentationDuration",
263  SecondsToXmlDuration(GetStaticMpdDuration()));
264 }
265 
266 void MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) {
267  DCHECK(mpd_node);
268  DCHECK_EQ(MpdType::kDynamic, mpd_options_.mpd_type);
269 
270  static const char kDynamicMpdType[] = "dynamic";
271  mpd_node->SetStringAttribute("type", kDynamicMpdType);
272 
273  // No offset from NOW.
274  mpd_node->SetStringAttribute("publishTime",
275  XmlDateTimeNowWithOffset(0, clock_.get()));
276 
277  // 'availabilityStartTime' is required for dynamic profile. Calculate if
278  // not already calculated.
279  if (availability_start_time_.empty()) {
280  double earliest_presentation_time;
281  if (GetEarliestTimestamp(&earliest_presentation_time)) {
282  availability_start_time_ = XmlDateTimeNowWithOffset(
283  -std::ceil(earliest_presentation_time), clock_.get());
284  } else {
285  LOG(ERROR) << "Could not determine the earliest segment presentation "
286  "time for availabilityStartTime calculation.";
287  // TODO(tinskip). Propagate an error.
288  }
289  }
290  if (!availability_start_time_.empty())
291  mpd_node->SetStringAttribute("availabilityStartTime",
292  availability_start_time_);
293 
294  if (Positive(mpd_options_.mpd_params.minimum_update_period)) {
295  mpd_node->SetStringAttribute(
296  "minimumUpdatePeriod",
297  SecondsToXmlDuration(mpd_options_.mpd_params.minimum_update_period));
298  } else {
299  LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod "
300  "specified.";
301  }
302 
303  SetIfPositive("timeShiftBufferDepth",
304  mpd_options_.mpd_params.time_shift_buffer_depth, mpd_node);
305  SetIfPositive("suggestedPresentationDelay",
306  mpd_options_.mpd_params.suggested_presentation_delay, mpd_node);
307 }
308 
309 float MpdBuilder::GetStaticMpdDuration() {
310  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
311 
312  if (periods_.empty()) {
313  LOG(WARNING) << "No Period found. Set MPD duration to 0.";
314  return 0.0f;
315  }
316 
317  // Attribute mediaPresentationDuration must be present for 'static' MPD. So
318  // setting "PT0S" is required even if none of the representaions have duration
319  // attribute.
320  float max_duration = 0.0f;
321 
322  // TODO(kqyang): Right now all periods contain the duration for the whole MPD.
323  // Simply get the duration from the first period. Ideally the period duration
324  // should only count the (sub)segments in that period.
325  for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
326  for (const auto* representation : adaptation_set->GetRepresentations()) {
327  max_duration =
328  std::max(representation->GetDurationSeconds(), max_duration);
329  }
330  }
331  return max_duration;
332 }
333 
334 bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) {
335  DCHECK(timestamp_seconds);
336  DCHECK(!periods_.empty());
337  double timestamp = 0;
338  double earliest_timestamp = -1;
339  // The first period should have the earliest timestamp.
340  for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
341  for (const auto* representation : adaptation_set->GetRepresentations()) {
342  if (representation->GetEarliestTimestamp(&timestamp) &&
343  (earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
344  earliest_timestamp = timestamp;
345  }
346  }
347  }
348  if (earliest_timestamp < 0)
349  return false;
350  *timestamp_seconds = earliest_timestamp;
351  return true;
352 }
353 
354 void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path,
355  MediaInfo* media_info) {
356  DCHECK(media_info);
357  const std::string kFileProtocol("file://");
358  std::string mpd_file_path = (mpd_path.find(kFileProtocol) == 0)
359  ? mpd_path.substr(kFileProtocol.size())
360  : mpd_path;
361 
362  if (!mpd_file_path.empty()) {
363  const FilePath mpd_dir(FilePath::FromUTF8Unsafe(mpd_file_path)
364  .DirName()
365  .AsEndingWithSeparator());
366  if (!mpd_dir.empty()) {
367  if (media_info->has_media_file_name()) {
368  media_info->set_media_file_name(
369  MakePathRelative(media_info->media_file_name(), mpd_dir));
370  }
371  if (media_info->has_init_segment_name()) {
372  media_info->set_init_segment_name(
373  MakePathRelative(media_info->init_segment_name(), mpd_dir));
374  }
375  if (media_info->has_segment_template()) {
376  media_info->set_segment_template(
377  MakePathRelative(media_info->segment_template(), mpd_dir));
378  }
379  }
380  }
381 }
382 
383 } // namespace shaka
double min_buffer_time
Definition: mpd_params.h:27
scoped_xml_ptr< xmlNode > PassScopedPtr()
Definition: xml_node.cc:136
All the methods that are virtual are virtual for mocking.
void SetStringAttribute(const char *attribute_name, const std::string &attribute)
Definition: xml_node.cc:104
bool AddChild(scoped_xml_ptr< xmlNode > child)
Definition: xml_node.cc:62
xmlNodePtr Release()
Definition: xml_node.cc:142
virtual Period * GetOrCreatePeriod(double start_time_in_seconds)
Definition: mpd_builder.cc:124
MpdBuilder(const MpdOptions &mpd_options)
Definition: mpd_builder.cc:115
void AddBaseUrl(const std::string &base_url)
Definition: mpd_builder.cc:120
virtual bool ToString(std::string *output)
Definition: mpd_builder.cc:139
static void MakePathsRelativeToMpd(const std::string &mpd_path, MediaInfo *media_info)
Definition: mpd_builder.cc:354
double minimum_update_period
Definition: mpd_params.h:42
Defines Mpd Options.
Definition: mpd_options.h:25
void SetContent(const std::string &content)
Definition: xml_node.cc:131
double time_shift_buffer_depth
Definition: mpd_params.h:33