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