Shaka Packager SDK
Loading...
Searching...
No Matches
representation.cc
1// Copyright 2017 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/representation.h>
8
9#include <algorithm>
10#include <cstdint>
11#include <cstdlib>
12#include <list>
13#include <memory>
14#include <optional>
15#include <string>
16#include <utility>
17
18#include <absl/log/check.h>
19#include <absl/log/log.h>
20#include <absl/strings/str_format.h>
21
22#include <packager/file.h>
23#include <packager/media/base/muxer_util.h>
24#include <packager/mpd/base/media_info.pb.h>
25#include <packager/mpd/base/mpd_options.h>
26#include <packager/mpd/base/mpd_utils.h>
27#include <packager/mpd/base/xml/xml_node.h>
28
29namespace shaka {
30namespace {
31
32std::string GetMimeType(const std::string& prefix,
33 MediaInfo::ContainerType container_type) {
34 switch (container_type) {
35 case MediaInfo::CONTAINER_MP4:
36 return prefix + "/mp4";
37 case MediaInfo::CONTAINER_MPEG2_TS:
38 // NOTE: DASH MPD spec uses lowercase but RFC3555 says uppercase.
39 return prefix + "/MP2T";
40 case MediaInfo::CONTAINER_WEBM:
41 return prefix + "/webm";
42 default:
43 break;
44 }
45
46 // Unsupported container types should be rejected/handled by the caller.
47 LOG(ERROR) << "Unrecognized container type: " << container_type;
48 return std::string();
49}
50
51// Check whether the video info has width and height.
52// DASH IOP also requires several other fields for video representations, namely
53// width, height, framerate, and sar.
54bool HasRequiredVideoFields(const MediaInfo_VideoInfo& video_info) {
55 if (!video_info.has_height() || !video_info.has_width()) {
56 LOG(ERROR)
57 << "Width and height are required fields for generating a valid MPD.";
58 return false;
59 }
60 // These fields are not required for a valid MPD, but required for DASH IOP
61 // compliant MPD. MpdBuilder can keep generating MPDs without these fields.
62 LOG_IF(WARNING, !video_info.has_time_scale())
63 << "Video info does not contain timescale required for "
64 "calculating framerate. @frameRate is required for DASH IOP.";
65 LOG_IF(WARNING, !video_info.has_pixel_width())
66 << "Video info does not contain pixel_width to calculate the sample "
67 "aspect ratio required for DASH IOP.";
68 LOG_IF(WARNING, !video_info.has_pixel_height())
69 << "Video info does not contain pixel_height to calculate the sample "
70 "aspect ratio required for DASH IOP.";
71 return true;
72}
73
74int32_t GetTimeScale(const MediaInfo& media_info) {
75 if (media_info.has_reference_time_scale()) {
76 return media_info.reference_time_scale();
77 }
78
79 if (media_info.has_video_info()) {
80 return media_info.video_info().time_scale();
81 }
82
83 if (media_info.has_audio_info()) {
84 return media_info.audio_info().time_scale();
85 }
86
87 LOG(WARNING) << "No timescale specified, using 1 as timescale.";
88 return 1;
89}
90
91} // namespace
92
94 const MediaInfo& media_info,
95 const MpdOptions& mpd_options,
96 uint32_t id,
97 std::unique_ptr<RepresentationStateChangeListener> state_change_listener)
98 : media_info_(media_info),
99 id_(id),
100 mpd_options_(mpd_options),
101 state_change_listener_(std::move(state_change_listener)),
102 allow_approximate_segment_timeline_(
103 // TODO(kqyang): Need a better check. $Time is legitimate but not a
104 // template.
105 media_info.segment_template().find("$Time") == std::string::npos &&
106 mpd_options_.mpd_params.allow_approximate_segment_timeline) {}
107
109 const Representation& representation,
110 std::unique_ptr<RepresentationStateChangeListener> state_change_listener)
111 : Representation(representation.media_info_,
112 representation.mpd_options_,
113 representation.id_,
114 std::move(state_change_listener)) {
115 mime_type_ = representation.mime_type_;
116 codecs_ = representation.codecs_;
117}
118
119Representation::~Representation() {}
120
122 if (!AtLeastOneTrue(media_info_.has_video_info(),
123 media_info_.has_audio_info(),
124 media_info_.has_text_info())) {
125 // This is an error. Segment information can be in AdaptationSet, Period, or
126 // MPD but the interface does not provide a way to set them.
127 // See 5.3.9.1 ISO 23009-1:2012 for segment info.
128 LOG(ERROR) << "Representation needs one of video, audio, or text.";
129 return false;
130 }
131
132 if (MoreThanOneTrue(media_info_.has_video_info(),
133 media_info_.has_audio_info(),
134 media_info_.has_text_info())) {
135 LOG(ERROR) << "Only one of VideoInfo, AudioInfo, or TextInfo can be set.";
136 return false;
137 }
138
139 if (media_info_.container_type() == MediaInfo::CONTAINER_UNKNOWN) {
140 LOG(ERROR) << "'container_type' in MediaInfo cannot be CONTAINER_UNKNOWN.";
141 return false;
142 }
143
144 if (media_info_.has_video_info()) {
145 mime_type_ = GetVideoMimeType();
146 if (!HasRequiredVideoFields(media_info_.video_info())) {
147 LOG(ERROR) << "Missing required fields to create a video Representation.";
148 return false;
149 }
150 } else if (media_info_.has_audio_info()) {
151 mime_type_ = GetAudioMimeType();
152 } else if (media_info_.has_text_info()) {
153 mime_type_ = GetTextMimeType();
154 }
155
156 if (mime_type_.empty())
157 return false;
158
159 codecs_ = GetCodecs(media_info_);
160 supplemental_codecs_ = GetSupplementalCodecs(media_info_);
161 supplemental_profiles_ = GetSupplementalProfiles(media_info_);
162 return true;
163}
164
166 const ContentProtectionElement& content_protection_element) {
167 content_protection_elements_.push_back(content_protection_element);
168 RemoveDuplicateAttributes(&content_protection_elements_.back());
169}
170
171void Representation::UpdateContentProtectionPssh(const std::string& drm_uuid,
172 const std::string& pssh) {
173 UpdateContentProtectionPsshHelper(drm_uuid, pssh,
174 &content_protection_elements_);
175}
176
177void Representation::AddNewSegment(int64_t start_time,
178 int64_t duration,
179 uint64_t size,
180 int64_t segment_number) {
181 if (start_time == 0 && duration == 0) {
182 LOG(WARNING) << "Got segment with start_time and duration == 0. Ignoring.";
183 return;
184 }
185
186 // In order for the oldest segment to be accessible for at least
187 // |time_shift_buffer_depth| seconds, the latest segment should not be in the
188 // sliding window since the player could be playing any part of the latest
189 // segment. So the current segment duration is added to the sum of segment
190 // durations (in the manifest/playlist) after sliding the window.
191 SlideWindow();
192
193 if (state_change_listener_)
194 state_change_listener_->OnNewSegmentForRepresentation(start_time, duration);
195
196 AddSegmentInfo(start_time, duration, segment_number);
197
198 // Only update the buffer depth and bandwidth estimator when the full segment
199 // is completed. In the low latency case, only the first chunk in the segment
200 // has been written at this point. Therefore, we must wait until the entire
201 // segment has been written before updating buffer depth and bandwidth
202 // estimator.
203 if (!mpd_options_.mpd_params.low_latency_dash_mode) {
204 current_buffer_depth_ += segment_infos_.back().duration;
205
206 bandwidth_estimator_.AddBlock(size, static_cast<double>(duration) /
207 media_info_.reference_time_scale());
208 }
209}
210
211void Representation::UpdateCompletedSegment(int64_t duration, uint64_t size) {
212 if (!mpd_options_.mpd_params.low_latency_dash_mode) {
213 LOG(WARNING)
214 << "UpdateCompletedSegment is only applicable to low latency mode.";
215 return;
216 }
217
218 UpdateSegmentInfo(duration);
219
220 current_buffer_depth_ += segment_infos_.back().duration;
221
222 bandwidth_estimator_.AddBlock(
223 size, static_cast<double>(duration) / media_info_.reference_time_scale());
224}
225
226void Representation::SetSampleDuration(int32_t frame_duration) {
227 // Sample duration is used to generate approximate SegmentTimeline.
228 // Text is required to have exactly the same segment duration.
229 if (media_info_.has_audio_info() || media_info_.has_video_info())
230 frame_duration_ = frame_duration;
231
232 if (media_info_.has_video_info()) {
233 media_info_.mutable_video_info()->set_frame_duration(frame_duration);
234 if (state_change_listener_) {
235 state_change_listener_->OnSetFrameRateForRepresentation(
236 frame_duration, media_info_.video_info().time_scale());
237 }
238 }
239}
240
242 int64_t sd = mpd_options_.mpd_params.target_segment_duration *
243 media_info_.reference_time_scale();
244 if (sd <= 0)
245 return;
246 media_info_.set_segment_duration(sd);
247}
248
249const MediaInfo& Representation::GetMediaInfo() const {
250 return media_info_;
251}
252
253// Uses info in |media_info_| and |content_protection_elements_| to create a
254// "Representation" node.
255// MPD schema has strict ordering. The following must be done in order.
256// AddVideoInfo() (possibly adds FramePacking elements), AddAudioInfo() (Adds
257// AudioChannelConfig elements), AddContentProtectionElements*(), and
258// AddVODOnlyInfo() (Adds segment info).
259std::optional<xml::XmlNode> Representation::GetXml() {
260 if (!HasRequiredMediaInfoFields()) {
261 LOG(ERROR) << "MediaInfo missing required fields.";
262 return std::nullopt;
263 }
264
265 const uint64_t bandwidth = media_info_.has_bandwidth()
266 ? media_info_.bandwidth()
267 : bandwidth_estimator_.Max();
268
269 DCHECK(!(HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)));
270
271 xml::RepresentationXmlNode representation;
272 // Mandatory fields for Representation.
273 if (!representation.SetId(id_) ||
274 !representation.SetIntegerAttribute("bandwidth", bandwidth) ||
275 !(codecs_.empty() ||
276 representation.SetStringAttribute("codecs", codecs_)) ||
277 !representation.SetStringAttribute("mimeType", mime_type_)) {
278 return std::nullopt;
279 }
280
281 if (!supplemental_codecs_.empty() && !supplemental_profiles_.empty()) {
282 if (!representation.SetStringAttribute("scte214:supplementalCodecs",
283 supplemental_codecs_) ||
284 !representation.SetStringAttribute("scte214:supplementalProfiles",
285 supplemental_profiles_)) {
286 LOG(ERROR) << "Failed to add supplemental codecs/profiles to "
287 "Representation XML.";
288 }
289 }
290
291 const bool has_video_info = media_info_.has_video_info();
292 const bool has_audio_info = media_info_.has_audio_info();
293
294 if (has_video_info &&
295 !representation.AddVideoInfo(
296 media_info_.video_info(),
297 !(output_suppression_flags_ & kSuppressWidth),
298 !(output_suppression_flags_ & kSuppressHeight),
299 !(output_suppression_flags_ & kSuppressFrameRate))) {
300 LOG(ERROR) << "Failed to add video info to Representation XML.";
301 return std::nullopt;
302 }
303
304 if (has_audio_info &&
305 !representation.AddAudioInfo(media_info_.audio_info())) {
306 LOG(ERROR) << "Failed to add audio info to Representation XML.";
307 return std::nullopt;
308 }
309
310 if (!representation.AddContentProtectionElements(
311 content_protection_elements_)) {
312 return std::nullopt;
313 }
314
315 if (HasVODOnlyFields(media_info_) &&
316 !representation.AddVODOnlyInfo(
317 media_info_, mpd_options_.mpd_params.use_segment_list,
318 mpd_options_.mpd_params.target_segment_duration)) {
319 LOG(ERROR) << "Failed to add VOD info.";
320 return std::nullopt;
321 }
322
323 if (HasLiveOnlyFields(media_info_) &&
324 !representation.AddLiveOnlyInfo(
325 media_info_, segment_infos_,
326 mpd_options_.mpd_params.low_latency_dash_mode)) {
327 LOG(ERROR) << "Failed to add Live info.";
328 return std::nullopt;
329 }
330 // TODO(rkuroiwa): It is likely that all representations have the exact same
331 // SegmentTemplate. Optimize and propagate the tag up to AdaptationSet level.
332
333 output_suppression_flags_ = 0;
334 return representation;
335}
336
337void Representation::SuppressOnce(SuppressFlag flag) {
338 output_suppression_flags_ |= flag;
339}
340
342 double presentation_time_offset) {
343 int64_t pto = presentation_time_offset * media_info_.reference_time_scale();
344 if (pto <= 0)
345 return;
346 media_info_.set_presentation_time_offset(pto);
347}
348
350 // Adjust the frame duration to units of seconds to match target segment
351 // duration.
352 const double frame_duration_sec =
353 (double)frame_duration_ / (double)media_info_.reference_time_scale();
354 // availabilityTimeOffset = segment duration - chunk duration.
355 // Here, the frame duration is equivalent to the sample duration,
356 // see Representation::SetSampleDuration(uint32_t frame_duration).
357 // By definition, each chunk will contain only one sample;
358 // thus, chunk_duration = sample_duration = frame_duration.
359 const double ato =
360 mpd_options_.mpd_params.target_segment_duration - frame_duration_sec;
361 if (ato <= 0)
362 return;
363 media_info_.set_availability_time_offset(ato);
364}
365
367 double* start_timestamp_seconds,
368 double* end_timestamp_seconds) const {
369 if (segment_infos_.empty())
370 return false;
371
372 if (start_timestamp_seconds) {
373 *start_timestamp_seconds =
374 static_cast<double>(segment_infos_.begin()->start_time) /
375 GetTimeScale(media_info_);
376 }
377 if (end_timestamp_seconds) {
378 *end_timestamp_seconds =
379 static_cast<double>(segment_infos_.rbegin()->start_time +
380 segment_infos_.rbegin()->duration *
381 (segment_infos_.rbegin()->repeat + 1)) /
382 GetTimeScale(media_info_);
383 }
384 return true;
385}
386
387bool Representation::HasRequiredMediaInfoFields() const {
388 if (HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)) {
389 LOG(ERROR) << "MediaInfo cannot have both VOD and Live fields.";
390 return false;
391 }
392
393 if (!media_info_.has_container_type()) {
394 LOG(ERROR) << "MediaInfo missing required field: container_type.";
395 return false;
396 }
397
398 return true;
399}
400
401void Representation::AddSegmentInfo(int64_t start_time,
402 int64_t duration,
403 int64_t segment_number) {
404 const uint64_t kNoRepeat = 0;
405 const int64_t adjusted_duration = AdjustDuration(duration);
406
407 if (!segment_infos_.empty()) {
408 // Contiguous segment.
409 const SegmentInfo& previous = segment_infos_.back();
410 const int64_t previous_segment_end_time =
411 previous.start_time + previous.duration * (previous.repeat + 1);
412 // Make it continuous if the segment start time is close to previous segment
413 // end time.
414 if (ApproximiatelyEqual(previous_segment_end_time, start_time)) {
415 const int64_t segment_end_time_for_same_duration =
416 previous_segment_end_time + previous.duration;
417 const int64_t actual_segment_end_time = start_time + duration;
418 // Consider the segments having identical duration if the segment end time
419 // is close to calculated segment end time by assuming identical duration.
420 if (ApproximiatelyEqual(segment_end_time_for_same_duration,
421 actual_segment_end_time)) {
422 ++segment_infos_.back().repeat;
423 } else {
424 segment_infos_.push_back(
425 {previous_segment_end_time,
426 actual_segment_end_time - previous_segment_end_time, kNoRepeat,
427 segment_number});
428 }
429 return;
430 }
431
432 // A gap since previous.
433 const int64_t kRoundingErrorGrace = 5;
434 if (previous_segment_end_time + kRoundingErrorGrace < start_time) {
435 LOG(WARNING) << RepresentationAsString() << " Found a gap of size "
436 << (start_time - previous_segment_end_time)
437 << " > kRoundingErrorGrace (" << kRoundingErrorGrace
438 << "). The new segment starts at " << start_time
439 << " but the previous segment ends at "
440 << previous_segment_end_time << ".";
441 }
442
443 // No overlapping segments.
444 if (start_time < previous_segment_end_time - kRoundingErrorGrace) {
445 LOG(WARNING)
446 << RepresentationAsString()
447 << " Segments should not be overlapping. The new segment starts at "
448 << start_time << " but the previous segment ends at "
449 << previous_segment_end_time << ".";
450 }
451 }
452 segment_infos_.push_back(
453 {start_time, adjusted_duration, kNoRepeat, segment_number});
454}
455
456void Representation::UpdateSegmentInfo(int64_t duration) {
457 if (!segment_infos_.empty()) {
458 // Update the duration in the current segment.
459 segment_infos_.back().duration = duration;
460 }
461}
462
463bool Representation::ApproximiatelyEqual(int64_t time1, int64_t time2) const {
464 if (!allow_approximate_segment_timeline_)
465 return time1 == time2;
466
467 // It is not always possible to align segment duration to target duration
468 // exactly. For example, for AAC with sampling rate of 44100, there are always
469 // 1024 audio samples per frame, so the frame duration is 1024/44100. For a
470 // target duration of 2 seconds, the closest segment duration would be 1.984
471 // or 2.00533.
472
473 // An arbitrary error threshold cap. This makes sure that the error is not too
474 // large for large samples.
475 const double kErrorThresholdSeconds = 0.05;
476
477 // So we consider two times equal if they differ by less than one sample.
478 const int32_t error_threshold =
479 std::min(frame_duration_,
480 static_cast<int32_t>(kErrorThresholdSeconds *
481 media_info_.reference_time_scale()));
482 return std::abs(time1 - time2) <= error_threshold;
483}
484
485int64_t Representation::AdjustDuration(int64_t duration) const {
486 if (!allow_approximate_segment_timeline_)
487 return duration;
488 const int64_t scaled_target_duration =
489 mpd_options_.mpd_params.target_segment_duration *
490 media_info_.reference_time_scale();
491 return ApproximiatelyEqual(scaled_target_duration, duration)
492 ? scaled_target_duration
493 : duration;
494}
495
496void Representation::SlideWindow() {
497 if (mpd_options_.mpd_params.time_shift_buffer_depth <= 0.0 ||
498 mpd_options_.mpd_type == MpdType::kStatic)
499 return;
500
501 const int32_t time_scale = GetTimeScale(media_info_);
502 DCHECK_GT(time_scale, 0);
503
504 const int64_t time_shift_buffer_depth = static_cast<int64_t>(
505 mpd_options_.mpd_params.time_shift_buffer_depth * time_scale);
506
507 if (current_buffer_depth_ <= time_shift_buffer_depth)
508 return;
509
510 std::list<SegmentInfo>::iterator first = segment_infos_.begin();
511 std::list<SegmentInfo>::iterator last = first;
512 for (; last != segment_infos_.end(); ++last) {
513 // Remove the current segment only if it falls completely out of time shift
514 // buffer range.
515 while (last->repeat >= 0 &&
516 current_buffer_depth_ - last->duration >= time_shift_buffer_depth) {
517 current_buffer_depth_ -= last->duration;
518 RemoveOldSegment(&*last);
519 }
520 if (last->repeat >= 0)
521 break;
522 }
523 segment_infos_.erase(first, last);
524}
525
526void Representation::RemoveOldSegment(SegmentInfo* segment_info) {
527 int64_t segment_start_time = segment_info->start_time;
528 segment_info->start_time += segment_info->duration;
529 segment_info->repeat--;
530 int64_t start_number = segment_info->start_segment_number;
531 segment_info->start_segment_number++;
532
533 if (mpd_options_.mpd_params.preserved_segments_outside_live_window == 0)
534 return;
535
536 segments_to_be_removed_.push_back(
537 media::GetSegmentName(media_info_.segment_template(), segment_start_time,
538 start_number, media_info_.bandwidth()));
539 while (segments_to_be_removed_.size() >
540 mpd_options_.mpd_params.preserved_segments_outside_live_window) {
541 const std::string& file_name = segments_to_be_removed_.front();
542 VLOG(2) << "Deleting " << file_name;
543 // DASH and HLS outputs could both be tracking the same files and are in a
544 // race to delete them. Delete() returns false if the file does not exist,
545 // but we only want to retry if the file does exist (indicating a failure
546 // to delete, rather than the file already being gone). GetFileSize()
547 // returns >= 0 if the file exists, and < 0 if it does not.
548 if (!File::Delete(file_name.c_str()) &&
549 File::GetFileSize(file_name.c_str()) >= 0) {
550 LOG(WARNING) << "Failed to delete " << file_name << "; Will retry later.";
551 break;
552 }
553 segments_to_be_removed_.pop_front();
554 }
555}
556
557std::string Representation::GetVideoMimeType() const {
558 return GetMimeType("video", media_info_.container_type());
559}
560
561std::string Representation::GetAudioMimeType() const {
562 return GetMimeType("audio", media_info_.container_type());
563}
564
565std::string Representation::GetTextMimeType() const {
566 CHECK(media_info_.has_text_info());
567 if (media_info_.text_info().codec() == "ttml") {
568 switch (media_info_.container_type()) {
569 case MediaInfo::CONTAINER_TEXT:
570 return "application/ttml+xml";
571 case MediaInfo::CONTAINER_MP4:
572 return "application/mp4";
573 default:
574 LOG(ERROR) << "Failed to determine MIME type for TTML container: "
575 << media_info_.container_type();
576 return "";
577 }
578 }
579 if (media_info_.text_info().codec() == "wvtt") {
580 if (media_info_.container_type() == MediaInfo::CONTAINER_TEXT) {
581 return "text/vtt";
582 } else if (media_info_.container_type() == MediaInfo::CONTAINER_MP4) {
583 return "application/mp4";
584 }
585 LOG(ERROR) << "Failed to determine MIME type for VTT container: "
586 << media_info_.container_type();
587 return "";
588 }
589
590 LOG(ERROR) << "Cannot determine MIME type for format: "
591 << media_info_.text_info().codec()
592 << " container: " << media_info_.container_type();
593 return "";
594}
595
596std::string Representation::RepresentationAsString() const {
597 std::string s = absl::StrFormat("Representation (id=%d,", id_);
598 if (media_info_.has_video_info()) {
599 const MediaInfo_VideoInfo& video_info = media_info_.video_info();
600 absl::StrAppendFormat(&s, "codec='%s',width=%d,height=%d",
601 video_info.codec().c_str(), video_info.width(),
602 video_info.height());
603 } else if (media_info_.has_audio_info()) {
604 const MediaInfo_AudioInfo& audio_info = media_info_.audio_info();
605 absl::StrAppendFormat(
606 &s, "codec='%s',frequency=%d,language='%s'", audio_info.codec().c_str(),
607 audio_info.sampling_frequency(), audio_info.language().c_str());
608 } else if (media_info_.has_text_info()) {
609 const MediaInfo_TextInfo& text_info = media_info_.text_info();
610 absl::StrAppendFormat(&s, "codec='%s',language='%s'",
611 text_info.codec().c_str(),
612 text_info.language().c_str());
613 }
614 absl::StrAppendFormat(&s, ")");
615 return s;
616}
617
618} // namespace shaka
void AddBlock(uint64_t size_in_bytes, double duration)
virtual void AddContentProtectionElement(const ContentProtectionElement &element)
virtual void AddNewSegment(int64_t start_time, int64_t duration, uint64_t size, int64_t segment_number)
virtual void UpdateContentProtectionPssh(const std::string &drm_uuid, const std::string &pssh)
virtual void UpdateCompletedSegment(int64_t duration, uint64_t size)
void SuppressOnce(SuppressFlag flag)
virtual const MediaInfo & GetMediaInfo() const
virtual void SetSampleDuration(int32_t sample_duration)
bool GetStartAndEndTimestamps(double *start_timestamp_seconds, double *end_timestamp_seconds) const
Representation(const MediaInfo &media_info, const MpdOptions &mpd_options, uint32_t representation_id, std::unique_ptr< RepresentationStateChangeListener > state_change_listener)
std::optional< xml::XmlNode > GetXml()
void SetPresentationTimeOffset(double presentation_time_offset)
Set @presentationTimeOffset in SegmentBase / SegmentTemplate.
RepresentationType in MPD.
Definition xml_node.h:187
bool AddVODOnlyInfo(const MediaInfo &media_info, bool use_segment_list, double target_segment_duration)
Definition xml_node.cc:422
bool AddLiveOnlyInfo(const MediaInfo &media_info, const std::list< SegmentInfo > &segment_infos, bool low_latency_dash_mode)
Definition xml_node.cc:494
bool AddAudioInfo(const MediaInfo::AudioInfo &audio_info)
Definition xml_node.cc:417
bool AddVideoInfo(const MediaInfo::VideoInfo &video_info, bool set_width, bool set_height, bool set_frame_rate)
Definition xml_node.cc:380
bool SetStringAttribute(const std::string &attribute_name, const std::string &attribute)
Definition xml_node.cc:211
bool SetId(uint32_t id)
Definition xml_node.cc:233
bool SetIntegerAttribute(const std::string &attribute_name, uint64_t number)
Definition xml_node.cc:218
All the methods that are virtual are virtual for mocking.
Defines Mpd Options.
Definition mpd_options.h:24