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/optional.h"
14 #include "packager/base/strings/string_number_conversions.h"
15 #include "packager/base/strings/stringprintf.h"
16 #include "packager/base/synchronization/lock.h"
17 #include "packager/base/time/default_clock.h"
18 #include "packager/base/time/time.h"
19 #include "packager/mpd/base/adaptation_set.h"
20 #include "packager/mpd/base/mpd_utils.h"
21 #include "packager/mpd/base/period.h"
22 #include "packager/mpd/base/representation.h"
23 #include "packager/mpd/base/xml/xml_node.h"
24 #include "packager/version/version.h"
25 
26 namespace shaka {
27 
28 using base::FilePath;
29 using xml::XmlNode;
30 
31 namespace {
32 
33 void AddMpdNameSpaceInfo(XmlNode* mpd) {
34  DCHECK(mpd);
35 
36  static const char kXmlNamespace[] = "urn:mpeg:dash:schema:mpd:2011";
37  static const char kXmlNamespaceXsi[] =
38  "http://www.w3.org/2001/XMLSchema-instance";
39  static const char kXmlNamespaceXlink[] = "http://www.w3.org/1999/xlink";
40  static const char kDashSchemaMpd2011[] =
41  "urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd";
42  static const char kCencNamespace[] = "urn:mpeg:cenc:2013";
43 
44  mpd->SetStringAttribute("xmlns", kXmlNamespace);
45  mpd->SetStringAttribute("xmlns:xsi", kXmlNamespaceXsi);
46  mpd->SetStringAttribute("xmlns:xlink", kXmlNamespaceXlink);
47  mpd->SetStringAttribute("xsi:schemaLocation", kDashSchemaMpd2011);
48  mpd->SetStringAttribute("xmlns:cenc", kCencNamespace);
49 }
50 
51 bool Positive(double d) {
52  return d > 0.0;
53 }
54 
55 // Return current time in XML DateTime format. The value is in UTC, so the
56 // string ends with a 'Z'.
57 std::string XmlDateTimeNowWithOffset(
58  int32_t offset_seconds,
59  base::Clock* clock) {
60  base::Time time = clock->Now();
61  time += base::TimeDelta::FromSeconds(offset_seconds);
62  base::Time::Exploded time_exploded;
63  time.UTCExplode(&time_exploded);
64 
65  return base::StringPrintf("%4d-%02d-%02dT%02d:%02d:%02dZ", time_exploded.year,
66  time_exploded.month, time_exploded.day_of_month,
67  time_exploded.hour, time_exploded.minute,
68  time_exploded.second);
69 }
70 
71 void SetIfPositive(const char* attr_name, double value, XmlNode* mpd) {
72  if (Positive(value)) {
73  mpd->SetStringAttribute(attr_name, SecondsToXmlDuration(value));
74  }
75 }
76 
77 std::string MakePathRelative(const std::string& media_path,
78  const FilePath& parent_path) {
79  FilePath relative_path;
80  const FilePath child_path = FilePath::FromUTF8Unsafe(media_path);
81  const bool is_child =
82  parent_path.AppendRelativePath(child_path, &relative_path);
83  if (!is_child)
84  relative_path = child_path;
85  return relative_path.NormalizePathSeparatorsTo('/').AsUTF8Unsafe();
86 }
87 
88 // Spooky static initialization/cleanup of libxml.
89 class LibXmlInitializer {
90  public:
91  LibXmlInitializer() : initialized_(false) {
92  base::AutoLock lock(lock_);
93  if (!initialized_) {
94  xmlInitParser();
95  initialized_ = true;
96  }
97  }
98 
99  ~LibXmlInitializer() {
100  base::AutoLock lock(lock_);
101  if (initialized_) {
102  xmlCleanupParser();
103  initialized_ = false;
104  }
105  }
106 
107  private:
108  base::Lock lock_;
109  bool initialized_;
110 
111  DISALLOW_COPY_AND_ASSIGN(LibXmlInitializer);
112 };
113 
114 } // namespace
115 
117  : mpd_options_(mpd_options), clock_(new base::DefaultClock()) {}
118 
119 MpdBuilder::~MpdBuilder() {}
120 
121 void MpdBuilder::AddBaseUrl(const std::string& base_url) {
122  base_urls_.push_back(base_url);
123 }
124 
125 Period* MpdBuilder::GetOrCreatePeriod(double start_time_in_seconds) {
126  for (auto& period : periods_) {
127  const double kPeriodTimeDriftThresholdInSeconds = 1.0;
128  const bool match =
129  std::fabs(period->start_time_in_seconds() - start_time_in_seconds) <
130  kPeriodTimeDriftThresholdInSeconds;
131  if (match)
132  return period.get();
133  }
134  periods_.emplace_back(new Period(period_counter_++, start_time_in_seconds,
135  mpd_options_, &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  bool output_period_duration = false;
176  if (mpd_options_.mpd_type == MpdType::kStatic) {
177  UpdatePeriodDurationAndPresentationTimestamp();
178  // Only output period duration if there are more than one period. In the
179  // case of only one period, Period@duration is redundant as it is identical
180  // to Mpd Duration so the convention is not to output Period@duration.
181  output_period_duration = periods_.size() > 1;
182  }
183 
184  for (const auto& period : periods_) {
185  xml::scoped_xml_ptr<xmlNode> period_node(
186  period->GetXml(output_period_duration));
187  if (!period_node || !mpd.AddChild(std::move(period_node)))
188  return nullptr;
189  }
190 
191  AddMpdNameSpaceInfo(&mpd);
192 
193  static const char kOnDemandProfile[] =
194  "urn:mpeg:dash:profile:isoff-on-demand:2011";
195  static const char kLiveProfile[] =
196  "urn:mpeg:dash:profile:isoff-live:2011";
197  switch (mpd_options_.dash_profile) {
198  case DashProfile::kOnDemand:
199  mpd.SetStringAttribute("profiles", kOnDemandProfile);
200  break;
201  case DashProfile::kLive:
202  mpd.SetStringAttribute("profiles", kLiveProfile);
203  break;
204  default:
205  NOTREACHED() << "Unknown DASH profile: "
206  << static_cast<int>(mpd_options_.dash_profile);
207  break;
208  }
209 
210  AddCommonMpdInfo(&mpd);
211  switch (mpd_options_.mpd_type) {
212  case MpdType::kStatic:
213  AddStaticMpdInfo(&mpd);
214  break;
215  case MpdType::kDynamic:
216  AddDynamicMpdInfo(&mpd);
217  // Must be after Period element.
218  AddUtcTiming(&mpd);
219  break;
220  default:
221  NOTREACHED() << "Unknown MPD type: "
222  << static_cast<int>(mpd_options_.mpd_type);
223  break;
224  }
225 
226  DCHECK(doc);
227  const std::string version = GetPackagerVersion();
228  if (!version.empty()) {
229  std::string version_string =
230  base::StringPrintf("Generated with %s version %s",
231  GetPackagerProjectUrl().c_str(), version.c_str());
232  xml::scoped_xml_ptr<xmlNode> comment(
233  xmlNewDocComment(doc.get(), BAD_CAST version_string.c_str()));
234  xmlDocSetRootElement(doc.get(), comment.get());
235  xmlAddSibling(comment.release(), mpd.Release());
236  } else {
237  xmlDocSetRootElement(doc.get(), mpd.Release());
238  }
239  return doc.release();
240 }
241 
242 void MpdBuilder::AddCommonMpdInfo(XmlNode* mpd_node) {
243  if (Positive(mpd_options_.mpd_params.min_buffer_time)) {
244  mpd_node->SetStringAttribute(
245  "minBufferTime",
246  SecondsToXmlDuration(mpd_options_.mpd_params.min_buffer_time));
247  } else {
248  LOG(ERROR) << "minBufferTime value not specified.";
249  // TODO(tinskip): Propagate error.
250  }
251 }
252 
253 void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) {
254  DCHECK(mpd_node);
255  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
256 
257  static const char kStaticMpdType[] = "static";
258  mpd_node->SetStringAttribute("type", kStaticMpdType);
259  mpd_node->SetStringAttribute("mediaPresentationDuration",
260  SecondsToXmlDuration(GetStaticMpdDuration()));
261 }
262 
263 void MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) {
264  DCHECK(mpd_node);
265  DCHECK_EQ(MpdType::kDynamic, mpd_options_.mpd_type);
266 
267  static const char kDynamicMpdType[] = "dynamic";
268  mpd_node->SetStringAttribute("type", kDynamicMpdType);
269 
270  // No offset from NOW.
271  mpd_node->SetStringAttribute("publishTime",
272  XmlDateTimeNowWithOffset(0, clock_.get()));
273 
274  // 'availabilityStartTime' is required for dynamic profile. Calculate if
275  // not already calculated.
276  if (availability_start_time_.empty()) {
277  double earliest_presentation_time;
278  if (GetEarliestTimestamp(&earliest_presentation_time)) {
279  availability_start_time_ = XmlDateTimeNowWithOffset(
280  -std::ceil(earliest_presentation_time), clock_.get());
281  } else {
282  LOG(ERROR) << "Could not determine the earliest segment presentation "
283  "time for availabilityStartTime calculation.";
284  // TODO(tinskip). Propagate an error.
285  }
286  }
287  if (!availability_start_time_.empty())
288  mpd_node->SetStringAttribute("availabilityStartTime",
289  availability_start_time_);
290 
291  if (Positive(mpd_options_.mpd_params.minimum_update_period)) {
292  mpd_node->SetStringAttribute(
293  "minimumUpdatePeriod",
294  SecondsToXmlDuration(mpd_options_.mpd_params.minimum_update_period));
295  } else {
296  LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod "
297  "specified.";
298  }
299 
300  SetIfPositive("timeShiftBufferDepth",
301  mpd_options_.mpd_params.time_shift_buffer_depth, mpd_node);
302  SetIfPositive("suggestedPresentationDelay",
303  mpd_options_.mpd_params.suggested_presentation_delay, mpd_node);
304 }
305 
306 void MpdBuilder::AddUtcTiming(XmlNode* mpd_node) {
307  DCHECK(mpd_node);
308  DCHECK_EQ(MpdType::kDynamic, mpd_options_.mpd_type);
309 
310  for (const MpdParams::UtcTiming& utc_timing :
311  mpd_options_.mpd_params.utc_timings) {
312  XmlNode utc_timing_node("UTCTiming");
313  utc_timing_node.SetStringAttribute("schemeIdUri", utc_timing.scheme_id_uri);
314  utc_timing_node.SetStringAttribute("value", utc_timing.value);
315  mpd_node->AddChild(utc_timing_node.PassScopedPtr());
316  }
317 }
318 
319 float MpdBuilder::GetStaticMpdDuration() {
320  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
321 
322  float total_duration = 0.0f;
323  for (const auto& period : periods_) {
324  total_duration += period->duration_seconds();
325  }
326  return total_duration;
327 }
328 
329 bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) {
330  DCHECK(timestamp_seconds);
331  DCHECK(!periods_.empty());
332  double timestamp = 0;
333  double earliest_timestamp = -1;
334  // TODO(kqyang): This is used to set availabilityStartTime. We may consider
335  // set presentationTimeOffset in the Representations then we can set
336  // availabilityStartTime to the time when MPD is first generated.
337  // The first period should have the earliest timestamp.
338  for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
339  for (const auto* representation : adaptation_set->GetRepresentations()) {
340  if (representation->GetStartAndEndTimestamps(&timestamp, nullptr) &&
341  (earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
342  earliest_timestamp = timestamp;
343  }
344  }
345  }
346  if (earliest_timestamp < 0)
347  return false;
348  *timestamp_seconds = earliest_timestamp;
349  return true;
350 }
351 
352 void MpdBuilder::UpdatePeriodDurationAndPresentationTimestamp() {
353  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
354 
355  for (const auto& period : periods_) {
356  std::list<Representation*> video_representations;
357  std::list<Representation*> non_video_representations;
358  for (const auto& adaptation_set : period->GetAdaptationSets()) {
359  const auto& representations = adaptation_set->GetRepresentations();
360  if (adaptation_set->IsVideo()) {
361  video_representations.insert(video_representations.end(),
362  representations.begin(),
363  representations.end());
364  } else {
365  non_video_representations.insert(non_video_representations.end(),
366  representations.begin(),
367  representations.end());
368  }
369  }
370 
371  base::Optional<double> earliest_start_time;
372  base::Optional<double> latest_end_time;
373  // The timestamps are based on Video Representations if exist.
374  const auto& representations = video_representations.size() > 0
375  ? video_representations
376  : non_video_representations;
377  for (const auto& representation : representations) {
378  double start_time = 0;
379  double end_time = 0;
380  if (representation->GetStartAndEndTimestamps(&start_time, &end_time)) {
381  earliest_start_time =
382  std::min(earliest_start_time.value_or(start_time), start_time);
383  latest_end_time =
384  std::max(latest_end_time.value_or(end_time), end_time);
385  }
386  }
387 
388  if (!earliest_start_time)
389  return;
390 
391  period->set_duration_seconds(*latest_end_time - *earliest_start_time);
392 
393  double presentation_time_offset = *earliest_start_time;
394  for (const auto& adaptation_set : period->GetAdaptationSets()) {
395  for (const auto& representation : adaptation_set->GetRepresentations()) {
396  representation->SetPresentationTimeOffset(presentation_time_offset);
397  }
398  }
399  }
400 }
401 
402 void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path,
403  MediaInfo* media_info) {
404  DCHECK(media_info);
405  const std::string kFileProtocol("file://");
406  std::string mpd_file_path = (mpd_path.find(kFileProtocol) == 0)
407  ? mpd_path.substr(kFileProtocol.size())
408  : mpd_path;
409 
410  if (!mpd_file_path.empty()) {
411  const FilePath mpd_dir(FilePath::FromUTF8Unsafe(mpd_file_path)
412  .DirName()
413  .AsEndingWithSeparator());
414  if (!mpd_dir.empty()) {
415  if (media_info->has_media_file_name()) {
416  media_info->set_media_file_url(
417  MakePathRelative(media_info->media_file_name(), mpd_dir));
418  }
419  if (media_info->has_init_segment_name()) {
420  media_info->set_init_segment_url(
421  MakePathRelative(media_info->init_segment_name(), mpd_dir));
422  }
423  if (media_info->has_segment_template()) {
424  media_info->set_segment_template_url(
425  MakePathRelative(media_info->segment_template(), mpd_dir));
426  }
427  }
428  }
429 }
430 
431 } // namespace shaka
double min_buffer_time
Definition: mpd_params.h:27
UTCTimings. For dynamic MPD only.
Definition: mpd_params.h:48
scoped_xml_ptr< xmlNode > PassScopedPtr()
Definition: xml_node.cc:169
All the methods that are virtual are virtual for mocking.
void SetStringAttribute(const char *attribute_name, const std::string &attribute)
Definition: xml_node.cc:137
bool AddChild(scoped_xml_ptr< xmlNode > child)
Definition: xml_node.cc:95
xmlNodePtr Release()
Definition: xml_node.cc:175
virtual Period * GetOrCreatePeriod(double start_time_in_seconds)
Definition: mpd_builder.cc:125
MpdBuilder(const MpdOptions &mpd_options)
Definition: mpd_builder.cc:116
void AddBaseUrl(const std::string &base_url)
Definition: mpd_builder.cc:121
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:402
double minimum_update_period
Definition: mpd_params.h:30
Defines Mpd Options.
Definition: mpd_options.h:25
void SetContent(const std::string &content)
Definition: xml_node.cc:164
double time_shift_buffer_depth
Definition: mpd_params.h:39