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