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