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 <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
30namespace shaka {
31
32using xml::XmlNode;
33
34namespace {
35
36bool 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
75bool 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'.
81std::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
91bool 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.
97class 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
127MpdBuilder::~MpdBuilder() {}
128
129void MpdBuilder::AddBaseUrl(const std::string& base_url) {
130 base_urls_.push_back(base_url);
131}
132
133Period* 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
147bool 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
164std::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[] = "urn:mpeg:dash:profile:isoff-live:2011";
197 switch (mpd_options_.dash_profile) {
198 case DashProfile::kOnDemand:
199 if (!mpd.SetStringAttribute("profiles", kOnDemandProfile))
200 return std::nullopt;
201 break;
202 case DashProfile::kLive:
203 if (!mpd.SetStringAttribute("profiles", kLiveProfile))
204 return std::nullopt;
205 break;
206 default:
207 NOTIMPLEMENTED() << "Unknown DASH profile: "
208 << static_cast<int>(mpd_options_.dash_profile);
209 break;
210 }
211
212 if (!AddCommonMpdInfo(&mpd))
213 return std::nullopt;
214 switch (mpd_options_.mpd_type) {
215 case MpdType::kStatic:
216 if (!AddStaticMpdInfo(&mpd))
217 return std::nullopt;
218 break;
219 case MpdType::kDynamic:
220 if (!AddDynamicMpdInfo(&mpd))
221 return std::nullopt;
222 // Must be after Period element.
223 if (!AddUtcTiming(&mpd))
224 return std::nullopt;
225 break;
226 default:
227 NOTIMPLEMENTED() << "Unknown MPD type: "
228 << static_cast<int>(mpd_options_.mpd_type);
229 break;
230 }
231 return mpd;
232}
233
234bool MpdBuilder::AddCommonMpdInfo(XmlNode* mpd_node) {
235 if (Positive(mpd_options_.mpd_params.min_buffer_time)) {
236 RCHECK(mpd_node->SetStringAttribute(
237 "minBufferTime",
238 SecondsToXmlDuration(mpd_options_.mpd_params.min_buffer_time)));
239 } else {
240 LOG(ERROR) << "minBufferTime value not specified.";
241 return false;
242 }
243 return true;
244}
245
246bool MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) {
247 DCHECK(mpd_node);
248 DCHECK_EQ(static_cast<int>(MpdType::kStatic),
249 static_cast<int>(mpd_options_.mpd_type));
250
251 static const char kStaticMpdType[] = "static";
252 return mpd_node->SetStringAttribute("type", kStaticMpdType) &&
253 mpd_node->SetStringAttribute(
254 "mediaPresentationDuration",
255 SecondsToXmlDuration(GetStaticMpdDuration()));
256}
257
258bool MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) {
259 DCHECK(mpd_node);
260 DCHECK_EQ(static_cast<int>(MpdType::kDynamic),
261 static_cast<int>(mpd_options_.mpd_type));
262
263 static const char kDynamicMpdType[] = "dynamic";
264 RCHECK(mpd_node->SetStringAttribute("type", kDynamicMpdType));
265
266 // No offset from NOW.
267 RCHECK(mpd_node->SetStringAttribute(
268 "publishTime", XmlDateTimeNowWithOffset(0, clock_.get())));
269
270 // 'availabilityStartTime' is required for dynamic profile. Calculate if
271 // not already calculated.
272 if (availability_start_time_.empty()) {
273 double earliest_presentation_time;
274 if (GetEarliestTimestamp(&earliest_presentation_time)) {
275 availability_start_time_ = XmlDateTimeNowWithOffset(
276 -std::ceil(earliest_presentation_time), clock_.get());
277 } else {
278 LOG(ERROR) << "Could not determine the earliest segment presentation "
279 "time for availabilityStartTime calculation.";
280 // TODO(tinskip). Propagate an error.
281 }
282 }
283 if (!availability_start_time_.empty()) {
284 RCHECK(mpd_node->SetStringAttribute("availabilityStartTime",
285 availability_start_time_));
286 }
287
288 if (Positive(mpd_options_.mpd_params.minimum_update_period)) {
289 RCHECK(mpd_node->SetStringAttribute(
290 "minimumUpdatePeriod",
291 SecondsToXmlDuration(mpd_options_.mpd_params.minimum_update_period)));
292 } else {
293 LOG(WARNING) << "The profile is dynamic but no minimumUpdatePeriod "
294 "specified.";
295 }
296
297 return SetIfPositive("timeShiftBufferDepth",
298 mpd_options_.mpd_params.time_shift_buffer_depth,
299 mpd_node) &&
300 SetIfPositive("suggestedPresentationDelay",
301 mpd_options_.mpd_params.suggested_presentation_delay,
302 mpd_node);
303}
304
305bool MpdBuilder::AddUtcTiming(XmlNode* mpd_node) {
306 DCHECK(mpd_node);
307 DCHECK_EQ(static_cast<int>(MpdType::kDynamic),
308 static_cast<int>(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 RCHECK(utc_timing_node.SetStringAttribute("schemeIdUri",
314 utc_timing.scheme_id_uri));
315 RCHECK(utc_timing_node.SetStringAttribute("value", utc_timing.value));
316 RCHECK(mpd_node->AddChild(std::move(utc_timing_node)));
317 }
318 return true;
319}
320
321float MpdBuilder::GetStaticMpdDuration() {
322 DCHECK_EQ(static_cast<int>(MpdType::kStatic),
323 static_cast<int>(mpd_options_.mpd_type));
324
325 float total_duration = 0.0f;
326 for (const auto& period : periods_) {
327 total_duration += period->duration_seconds();
328 }
329 return total_duration;
330}
331
333 if (mpd_options_.mpd_params.event_to_vod_on_end_of_stream) {
334 mpd_options_.dash_profile = DashProfile::kOnDemand;
335 mpd_options_.mpd_type = MpdType::kStatic;
336 }
337}
338
339bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) {
340 DCHECK(timestamp_seconds);
341 DCHECK(!periods_.empty());
342 if (periods_.empty())
343 return false;
344 double timestamp = 0;
345 double earliest_timestamp = -1;
346 // TODO(kqyang): This is used to set availabilityStartTime. We may consider
347 // set presentationTimeOffset in the Representations then we can set
348 // availabilityStartTime to the time when MPD is first generated.
349 // The first period should have the earliest timestamp.
350 for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) {
351 for (const auto* representation : adaptation_set->GetRepresentations()) {
352 if (representation->GetStartAndEndTimestamps(&timestamp, nullptr) &&
353 (earliest_timestamp < 0 || timestamp < earliest_timestamp)) {
354 earliest_timestamp = timestamp;
355 }
356 }
357 }
358 if (earliest_timestamp < 0)
359 return false;
360 *timestamp_seconds = earliest_timestamp;
361 return true;
362}
363
364void MpdBuilder::UpdatePeriodDurationAndPresentationTimestamp() {
365 DCHECK_EQ(static_cast<int>(MpdType::kStatic),
366 static_cast<int>(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 std::optional<double> earliest_start_time;
385 std::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
415void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path,
416 MediaInfo* media_info) {
417 DCHECK(media_info);
418 const std::string kFileProtocol("file://");
419 std::filesystem::path mpd_file_path =
420 (mpd_path.find(kFileProtocol) == 0)
421 ? mpd_path.substr(kFileProtocol.size())
422 : mpd_path;
423
424 if (!mpd_file_path.empty()) {
425 const std::filesystem::path mpd_dir(mpd_file_path.parent_path());
426 if (media_info->has_media_file_name()) {
427 media_info->set_media_file_url(
428 MakePathRelative(media_info->media_file_name(), mpd_dir));
429 }
430 if (media_info->has_init_segment_name()) {
431 media_info->set_init_segment_url(
432 MakePathRelative(media_info->init_segment_name(), mpd_dir));
433 }
434 if (media_info->has_segment_template()) {
435 media_info->set_segment_template_url(
436 MakePathRelative(media_info->segment_template(), mpd_dir));
437 }
438 }
439}
440
441} // 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:25