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_.GetNext(),
135  start_time_in_seconds, mpd_options_,
136  &representation_counter_));
137  return periods_.back().get();
138 }
139 
140 bool MpdBuilder::ToString(std::string* output) {
141  DCHECK(output);
142  static LibXmlInitializer lib_xml_initializer;
143 
144  xml::scoped_xml_ptr<xmlDoc> doc(GenerateMpd());
145  if (!doc)
146  return false;
147 
148  static const int kNiceFormat = 1;
149  int doc_str_size = 0;
150  xmlChar* doc_str = nullptr;
151  xmlDocDumpFormatMemoryEnc(doc.get(), &doc_str, &doc_str_size, "UTF-8",
152  kNiceFormat);
153  output->assign(doc_str, doc_str + doc_str_size);
154  xmlFree(doc_str);
155 
156  // Cleanup, free the doc.
157  doc.reset();
158  return true;
159 }
160 
161 xmlDocPtr MpdBuilder::GenerateMpd() {
162  // Setup nodes.
163  static const char kXmlVersion[] = "1.0";
164  xml::scoped_xml_ptr<xmlDoc> doc(xmlNewDoc(BAD_CAST kXmlVersion));
165  XmlNode mpd("MPD");
166 
167  // Add baseurls to MPD.
168  for (const std::string& base_url : base_urls_) {
169  XmlNode xml_base_url("BaseURL");
170  xml_base_url.SetContent(base_url);
171 
172  if (!mpd.AddChild(xml_base_url.PassScopedPtr()))
173  return nullptr;
174  }
175 
176  bool output_period_duration = false;
177  if (mpd_options_.mpd_type == MpdType::kStatic) {
178  UpdatePeriodDurationAndPresentationTimestamp();
179  // Only output period duration if there are more than one period. In the
180  // case of only one period, Period@duration is redundant as it is identical
181  // to Mpd Duration so the convention is not to output Period@duration.
182  output_period_duration = periods_.size() > 1;
183  }
184 
185  for (const auto& period : periods_) {
186  xml::scoped_xml_ptr<xmlNode> period_node(
187  period->GetXml(output_period_duration));
188  if (!period_node || !mpd.AddChild(std::move(period_node)))
189  return nullptr;
190  }
191 
192  AddMpdNameSpaceInfo(&mpd);
193 
194  static const char kOnDemandProfile[] =
195  "urn:mpeg:dash:profile:isoff-on-demand:2011";
196  static const char kLiveProfile[] =
197  "urn:mpeg:dash:profile:isoff-live:2011";
198  switch (mpd_options_.dash_profile) {
199  case DashProfile::kOnDemand:
200  mpd.SetStringAttribute("profiles", kOnDemandProfile);
201  break;
202  case DashProfile::kLive:
203  mpd.SetStringAttribute("profiles", kLiveProfile);
204  break;
205  default:
206  NOTREACHED() << "Unknown DASH profile: "
207  << static_cast<int>(mpd_options_.dash_profile);
208  break;
209  }
210 
211  AddCommonMpdInfo(&mpd);
212  switch (mpd_options_.mpd_type) {
213  case MpdType::kStatic:
214  AddStaticMpdInfo(&mpd);
215  break;
216  case MpdType::kDynamic:
217  AddDynamicMpdInfo(&mpd);
218  // Must be after Period element.
219  AddUtcTiming(&mpd);
220  break;
221  default:
222  NOTREACHED() << "Unknown MPD type: "
223  << static_cast<int>(mpd_options_.mpd_type);
224  break;
225  }
226 
227  DCHECK(doc);
228  const std::string version = GetPackagerVersion();
229  if (!version.empty()) {
230  std::string version_string =
231  base::StringPrintf("Generated with %s version %s",
232  GetPackagerProjectUrl().c_str(), version.c_str());
233  xml::scoped_xml_ptr<xmlNode> comment(
234  xmlNewDocComment(doc.get(), BAD_CAST version_string.c_str()));
235  xmlDocSetRootElement(doc.get(), comment.get());
236  xmlAddSibling(comment.release(), mpd.Release());
237  } else {
238  xmlDocSetRootElement(doc.get(), mpd.Release());
239  }
240  return doc.release();
241 }
242 
243 void MpdBuilder::AddCommonMpdInfo(XmlNode* mpd_node) {
244  if (Positive(mpd_options_.mpd_params.min_buffer_time)) {
245  mpd_node->SetStringAttribute(
246  "minBufferTime",
247  SecondsToXmlDuration(mpd_options_.mpd_params.min_buffer_time));
248  } else {
249  LOG(ERROR) << "minBufferTime value not specified.";
250  // TODO(tinskip): Propagate error.
251  }
252 }
253 
254 void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) {
255  DCHECK(mpd_node);
256  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
257 
258  static const char kStaticMpdType[] = "static";
259  mpd_node->SetStringAttribute("type", kStaticMpdType);
260  mpd_node->SetStringAttribute("mediaPresentationDuration",
261  SecondsToXmlDuration(GetStaticMpdDuration()));
262 }
263 
264 void MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) {
265  DCHECK(mpd_node);
266  DCHECK_EQ(MpdType::kDynamic, mpd_options_.mpd_type);
267 
268  static const char kDynamicMpdType[] = "dynamic";
269  mpd_node->SetStringAttribute("type", kDynamicMpdType);
270 
271  // No offset from NOW.
272  mpd_node->SetStringAttribute("publishTime",
273  XmlDateTimeNowWithOffset(0, clock_.get()));
274 
275  // 'availabilityStartTime' is required for dynamic profile. Calculate if
276  // not already calculated.
277  if (availability_start_time_.empty()) {
278  double earliest_presentation_time;
279  if (GetEarliestTimestamp(&earliest_presentation_time)) {
280  availability_start_time_ = XmlDateTimeNowWithOffset(
281  -std::ceil(earliest_presentation_time), clock_.get());
282  } else {
283  LOG(ERROR) << "Could not determine the earliest segment presentation "
284  "time for availabilityStartTime calculation.";
285  // TODO(tinskip). Propagate an error.
286  }
287  }
288  if (!availability_start_time_.empty())
289  mpd_node->SetStringAttribute("availabilityStartTime",
290  availability_start_time_);
291 
292  if (Positive(mpd_options_.mpd_params.minimum_update_period)) {
293  mpd_node->SetStringAttribute(
294  "minimumUpdatePeriod",
295  SecondsToXmlDuration(mpd_options_.mpd_params.minimum_update_period));
296  } else {
297  LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod "
298  "specified.";
299  }
300 
301  SetIfPositive("timeShiftBufferDepth",
302  mpd_options_.mpd_params.time_shift_buffer_depth, mpd_node);
303  SetIfPositive("suggestedPresentationDelay",
304  mpd_options_.mpd_params.suggested_presentation_delay, mpd_node);
305 }
306 
307 void MpdBuilder::AddUtcTiming(XmlNode* mpd_node) {
308  DCHECK(mpd_node);
309  DCHECK_EQ(MpdType::kDynamic, mpd_options_.mpd_type);
310 
311  for (const MpdParams::UtcTiming& utc_timing :
312  mpd_options_.mpd_params.utc_timings) {
313  XmlNode utc_timing_node("UTCTiming");
314  utc_timing_node.SetStringAttribute("schemeIdUri", utc_timing.scheme_id_uri);
315  utc_timing_node.SetStringAttribute("value", utc_timing.value);
316  mpd_node->AddChild(utc_timing_node.PassScopedPtr());
317  }
318 }
319 
320 float MpdBuilder::GetStaticMpdDuration() {
321  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
322 
323  float total_duration = 0.0f;
324  for (const auto& period : periods_) {
325  total_duration += period->duration_seconds();
326  }
327  return total_duration;
328 }
329 
330 bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) {
331  DCHECK(timestamp_seconds);
332  DCHECK(!periods_.empty());
333  double timestamp = 0;
334  double earliest_timestamp = -1;
335  // TODO(kqyang): This is used to set availabilityStartTime. We may consider
336  // set presentationTimeOffset in the Representations then we can set
337  // availabilityStartTime to the time when MPD is first generated.
338  // The first period should have the earliest timestamp.
339  for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
340  for (const auto* representation : adaptation_set->GetRepresentations()) {
341  if (representation->GetStartAndEndTimestamps(&timestamp, nullptr) &&
342  (earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
343  earliest_timestamp = timestamp;
344  }
345  }
346  }
347  if (earliest_timestamp < 0)
348  return false;
349  *timestamp_seconds = earliest_timestamp;
350  return true;
351 }
352 
353 void MpdBuilder::UpdatePeriodDurationAndPresentationTimestamp() {
354  DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type);
355 
356  for (const auto& period : periods_) {
357  std::list<Representation*> video_representations;
358  std::list<Representation*> non_video_representations;
359  for (const auto& adaptation_set : period->GetAdaptationSets()) {
360  const auto& representations = adaptation_set->GetRepresentations();
361  if (adaptation_set->IsVideo()) {
362  video_representations.insert(video_representations.end(),
363  representations.begin(),
364  representations.end());
365  } else {
366  non_video_representations.insert(non_video_representations.end(),
367  representations.begin(),
368  representations.end());
369  }
370  }
371 
372  base::Optional<double> earliest_start_time;
373  base::Optional<double> latest_end_time;
374  // The timestamps are based on Video Representations if exist.
375  const auto& representations = video_representations.size() > 0
376  ? video_representations
377  : non_video_representations;
378  for (const auto& representation : representations) {
379  double start_time = 0;
380  double end_time = 0;
381  if (representation->GetStartAndEndTimestamps(&start_time, &end_time)) {
382  earliest_start_time =
383  std::min(earliest_start_time.value_or(start_time), start_time);
384  latest_end_time =
385  std::max(latest_end_time.value_or(end_time), end_time);
386  }
387  }
388 
389  if (!earliest_start_time)
390  return;
391 
392  period->set_duration_seconds(*latest_end_time - *earliest_start_time);
393 
394  double presentation_time_offset = *earliest_start_time;
395  for (const auto& adaptation_set : period->GetAdaptationSets()) {
396  for (const auto& representation : adaptation_set->GetRepresentations()) {
397  representation->SetPresentationTimeOffset(presentation_time_offset);
398  }
399  }
400  }
401 }
402 
403 void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path,
404  MediaInfo* media_info) {
405  DCHECK(media_info);
406  const std::string kFileProtocol("file://");
407  std::string mpd_file_path = (mpd_path.find(kFileProtocol) == 0)
408  ? mpd_path.substr(kFileProtocol.size())
409  : mpd_path;
410 
411  if (!mpd_file_path.empty()) {
412  const FilePath mpd_dir(FilePath::FromUTF8Unsafe(mpd_file_path)
413  .DirName()
414  .AsEndingWithSeparator());
415  if (!mpd_dir.empty()) {
416  if (media_info->has_media_file_name()) {
417  media_info->set_media_file_url(
418  MakePathRelative(media_info->media_file_name(), mpd_dir));
419  }
420  if (media_info->has_init_segment_name()) {
421  media_info->set_init_segment_url(
422  MakePathRelative(media_info->init_segment_name(), mpd_dir));
423  }
424  if (media_info->has_segment_template()) {
425  media_info->set_segment_template_url(
426  MakePathRelative(media_info->segment_template(), mpd_dir));
427  }
428  }
429  }
430 }
431 
432 } // 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:140
static void MakePathsRelativeToMpd(const std::string &mpd_path, MediaInfo *media_info)
Definition: mpd_builder.cc:403
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