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