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
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
23namespace shaka {
24namespace {
25
26std::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.
48bool 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
68int32_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
113Representation::~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
165void Representation::UpdateContentProtectionPssh(const std::string& drm_uuid,
166 const std::string& pssh) {
167 UpdateContentProtectionPsshHelper(drm_uuid, pssh,
168 &content_protection_elements_);
169}
170
171void 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
205void 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
220void 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
243const 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).
253std::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
331void 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
381bool 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
395void 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
450void 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
457bool 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
479int64_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
490void 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
520void 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
545std::string Representation::GetVideoMimeType() const {
546 return GetMimeType("video", media_info_.container_type());
547}
548
549std::string Representation::GetAudioMimeType() const {
550 return GetMimeType("audio", media_info_.container_type());
551}
552
553std::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
584std::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.
Defines Mpd Options.
Definition mpd_options.h:25