From f8a1cb66ad2aaaa4cc96383fc2700060160f8f03 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Mon, 29 Jan 2018 10:37:50 -0800 Subject: [PATCH] Calculate presentationTimeOffset and Period@duration from segments Prefer timestamps from Video AdaptationSets if available - this avoids possible video playback jitters due to gaps. presentationTimeOffset is not applied to the first period as it may in negative dts which Chrome does not like: https://crbug.com/398141. It is safe to apply to subsequent periods as the actual offset applied takes Period@start into consideration: offset = Period@start - presentationTimeOffset The result timestamp with offset applied is close to Period@start, so it is unlikely to result in a negative dts value. Closes b/73899306. Change-Id: If8361f5469610093b3aac6675754536ad7e83c4c --- .../bear-320x240-opus-vp9-cenc-golden.mpd | 2 +- .../bear-320x240-vp9-opus-webm-golden.mpd | 2 +- .../bear-640x360-a-clear-v-cenc-golden.mpd | 2 +- .../testdata/bear-640x360-av-cbc1-golden.mpd | 2 +- .../testdata/bear-640x360-av-cbcs-golden.mpd | 2 +- .../bear-640x360-av-cenc-ad_cues-golden.mpd | 6 +- .../testdata/bear-640x360-av-cenc-golden.mpd | 2 +- ...r-640x360-av-cenc-no-clear-lead-golden.mpd | 2 +- .../bear-640x360-av-cenc-no-pssh-golden.mpd | 2 +- .../bear-640x360-av-cenc-non-iop-golden.mpd | 2 +- .../testdata/bear-640x360-av-cens-golden.mpd | 2 +- .../test/testdata/bear-640x360-av-golden.mpd | 2 +- ...-640x360-av-live-static-ad_cues-golden.mpd | 8 +- .../bear-640x360-av-live-static-golden.mpd | 2 +- .../bear-640x360-av-por-BR-golden.mpd | 2 +- .../testdata/bear-640x360-av-por-golden.mpd | 2 +- .../bear-640x360-av-trick-1-cenc-golden.mpd | 2 +- .../bear-640x360-av-trick-1-golden.mpd | 2 +- ...640x360-av-trick-1-trick-2-cenc-golden.mpd | 2 +- ...bear-640x360-av-trick-1-trick-2-golden.mpd | 2 +- .../test/testdata/bear-640x360-avt-golden.mpd | 2 +- packager/mpd/base/adaptation_set.cc | 13 +- packager/mpd/base/adaptation_set.h | 10 +- packager/mpd/base/adaptation_set_unittest.cc | 16 +-- packager/mpd/base/mock_mpd_builder.h | 5 +- packager/mpd/base/mpd_builder.cc | 118 +++++++++++++----- packager/mpd/base/mpd_builder.h | 3 + packager/mpd/base/mpd_builder_unittest.cc | 83 +++++++++--- packager/mpd/base/period.cc | 7 +- packager/mpd/base/period.h | 5 +- packager/mpd/base/period_unittest.cc | 20 ++- packager/mpd/base/representation.cc | 47 +++++-- packager/mpd/base/representation.h | 20 +-- packager/mpd/base/representation_unittest.cc | 71 +++++++---- packager/mpd/base/simple_mpd_notifier.cc | 22 +--- .../mpd/base/simple_mpd_notifier_unittest.cc | 9 +- .../audio_media_info1_expected_mpd_output.txt | 2 +- ..._video_media_info1_expected_mpd_output.txt | 2 +- .../video_media_info1_expected_mpd_output.txt | 2 +- ...eo_media_info1and2_expected_mpd_output.txt | 2 +- 40 files changed, 322 insertions(+), 187 deletions(-) diff --git a/packager/app/test/testdata/bear-320x240-opus-vp9-cenc-golden.mpd b/packager/app/test/testdata/bear-320x240-opus-vp9-cenc-golden.mpd index 232f14fba68..d488094f940 100644 --- a/packager/app/test/testdata/bear-320x240-opus-vp9-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-320x240-opus-vp9-cenc-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-320x240-vp9-opus-webm-golden.mpd b/packager/app/test/testdata/bear-320x240-vp9-opus-webm-golden.mpd index 4b34ee862f8..7cf286a0433 100644 --- a/packager/app/test/testdata/bear-320x240-vp9-opus-webm-golden.mpd +++ b/packager/app/test/testdata/bear-320x240-vp9-opus-webm-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-a-clear-v-cenc-golden.mpd b/packager/app/test/testdata/bear-640x360-a-clear-v-cenc-golden.mpd index 3b00bcdb1fa..d16ca12b0e5 100644 --- a/packager/app/test/testdata/bear-640x360-a-clear-v-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-a-clear-v-cenc-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cbc1-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cbc1-golden.mpd index 72c50df3797..ded974833f3 100644 --- a/packager/app/test/testdata/bear-640x360-av-cbc1-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cbc1-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd index 6349663e854..b2a8da16b4b 100644 --- a/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cbcs-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cenc-ad_cues-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cenc-ad_cues-golden.mpd index be58be5815e..fd13d808810 100644 --- a/packager/app/test/testdata/bear-640x360-av-cenc-ad_cues-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cenc-ad_cues-golden.mpd @@ -1,7 +1,7 @@ - - + + @@ -28,7 +28,7 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cenc-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cenc-golden.mpd index 8b257daeb9f..5f0b7c2d4ce 100644 --- a/packager/app/test/testdata/bear-640x360-av-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cenc-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cenc-no-clear-lead-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cenc-no-clear-lead-golden.mpd index 9c9d34c31b4..2c332d628b2 100644 --- a/packager/app/test/testdata/bear-640x360-av-cenc-no-clear-lead-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cenc-no-clear-lead-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cenc-no-pssh-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cenc-no-pssh-golden.mpd index b7400f71bbb..8a8f5fe9100 100644 --- a/packager/app/test/testdata/bear-640x360-av-cenc-no-pssh-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cenc-no-pssh-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cenc-non-iop-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cenc-non-iop-golden.mpd index be5e4496c67..ea6273a9a64 100644 --- a/packager/app/test/testdata/bear-640x360-av-cenc-non-iop-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cenc-non-iop-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd b/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd index 98ff74f0e18..fa83b269e19 100644 --- a/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-cens-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-golden.mpd b/packager/app/test/testdata/bear-640x360-av-golden.mpd index dfa8debf371..81bbf4f5948 100644 --- a/packager/app/test/testdata/bear-640x360-av-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-live-static-ad_cues-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-static-ad_cues-golden.mpd index 5d454a40451..9d299d7fa48 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-static-ad_cues-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-static-ad_cues-golden.mpd @@ -1,7 +1,7 @@ - - + + @@ -22,7 +22,7 @@ - + @@ -35,7 +35,7 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd b/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd index e4abe58270a..53e77e52589 100644 --- a/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-live-static-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-por-BR-golden.mpd b/packager/app/test/testdata/bear-640x360-av-por-BR-golden.mpd index fde8124b134..2b5041c5f88 100644 --- a/packager/app/test/testdata/bear-640x360-av-por-BR-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-por-BR-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-por-golden.mpd b/packager/app/test/testdata/bear-640x360-av-por-golden.mpd index fde8124b134..2b5041c5f88 100644 --- a/packager/app/test/testdata/bear-640x360-av-por-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-por-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-trick-1-cenc-golden.mpd b/packager/app/test/testdata/bear-640x360-av-trick-1-cenc-golden.mpd index c2a16fa0ef3..9c7d256e7b8 100644 --- a/packager/app/test/testdata/bear-640x360-av-trick-1-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-trick-1-cenc-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-trick-1-golden.mpd b/packager/app/test/testdata/bear-640x360-av-trick-1-golden.mpd index beb72c7c51d..47002fcf883 100644 --- a/packager/app/test/testdata/bear-640x360-av-trick-1-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-trick-1-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-cenc-golden.mpd b/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-cenc-golden.mpd index 6e3f9abbd39..711c66824b4 100644 --- a/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-cenc-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-cenc-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-golden.mpd b/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-golden.mpd index 8e9c3b7415a..6a5bbe82380 100644 --- a/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-av-trick-1-trick-2-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/app/test/testdata/bear-640x360-avt-golden.mpd b/packager/app/test/testdata/bear-640x360-avt-golden.mpd index 46380e8396c..0be7c145383 100644 --- a/packager/app/test/testdata/bear-640x360-avt-golden.mpd +++ b/packager/app/test/testdata/bear-640x360-avt-golden.mpd @@ -1,6 +1,6 @@ - + diff --git a/packager/mpd/base/adaptation_set.cc b/packager/mpd/base/adaptation_set.cc index 65b941ce639..b4f755c228f 100644 --- a/packager/mpd/base/adaptation_set.cc +++ b/packager/mpd/base/adaptation_set.cc @@ -201,15 +201,14 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { return representation_ptr; } -Representation* AdaptationSet::CopyRepresentationWithTimeOffset( - const Representation& representation, - uint64_t presentation_time_offset) { +Representation* AdaptationSet::CopyRepresentation( + const Representation& representation) { // Note that AdaptationSet outlive Representation, so this object // will die before AdaptationSet. std::unique_ptr listener( new RepresentationStateChangeListenerImpl(representation.id(), this)); - std::unique_ptr new_representation(new Representation( - representation, presentation_time_offset, std::move(listener))); + std::unique_ptr new_representation( + new Representation(representation, std::move(listener))); UpdateFromMediaInfo(new_representation->GetMediaInfo()); Representation* representation_ptr = new_representation.get(); @@ -384,6 +383,10 @@ const std::list AdaptationSet::GetRepresentations() const { return representations; } +bool AdaptationSet::IsVideo() const { + return content_type_ == "video"; +} + void AdaptationSet::UpdateFromMediaInfo(const MediaInfo& media_info) { // For videos, record the width, height, and the frame rate to calculate the // max {width,height,framerate} required for DASH IOP. diff --git a/packager/mpd/base/adaptation_set.h b/packager/mpd/base/adaptation_set.h index ea14fba9506..39d9e9c8e32 100644 --- a/packager/mpd/base/adaptation_set.h +++ b/packager/mpd/base/adaptation_set.h @@ -63,13 +63,10 @@ class AdaptationSet { /// AdaptationSet. One use case is to duplicate Representation in different /// periods. /// @param representation is an existing Representation to be cloned from. - /// @param presentation_time_offset is the presentation time offset for the - /// new Representation instance. /// @return On success, returns a pointer to Representation. Otherwise returns /// NULL. The returned pointer is owned by the AdaptationSet instance. - virtual Representation* CopyRepresentationWithTimeOffset( - const Representation& representation, - uint64_t presentation_time_offset); + virtual Representation* CopyRepresentation( + const Representation& representation); /// Add a ContenProtection element to the adaptation set. /// AdaptationSet does not add elements @@ -166,6 +163,9 @@ class AdaptationSet { // Return the list of Representations in this AdaptationSet. const std::list GetRepresentations() const; + /// @return true if it is a video AdaptationSet. + bool IsVideo() const; + protected: /// @param adaptation_set_id is an ID number for this AdaptationSet. /// @param lang is the language of this AdaptationSet. Mainly relevant for diff --git a/packager/mpd/base/adaptation_set_unittest.cc b/packager/mpd/base/adaptation_set_unittest.cc index bce79e37bec..13bf8a5535a 100644 --- a/packager/mpd/base/adaptation_set_unittest.cc +++ b/packager/mpd/base/adaptation_set_unittest.cc @@ -120,7 +120,7 @@ TEST_F(AdaptationSetTest, CheckAdaptationSetTextContentType) { AttributeEqual("contentType", "text")); } -TEST_F(AdaptationSetTest, CopyRepresentationWithTimeOffset) { +TEST_F(AdaptationSetTest, CopyRepresentation) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -137,12 +137,9 @@ TEST_F(AdaptationSetTest, CopyRepresentationWithTimeOffset) { Representation* representation = adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo)); - const uint64_t kPresentationTimeOffset = 80; Representation* new_representation = - adaptation_set->CopyRepresentationWithTimeOffset(*representation, - kPresentationTimeOffset); - EXPECT_EQ(kPresentationTimeOffset, - new_representation->GetMediaInfo().presentation_time_offset()); + adaptation_set->CopyRepresentation(*representation); + ASSERT_TRUE(new_representation); } // Verify that language passed to the constructor sets the @lang field is set. @@ -627,13 +624,10 @@ TEST_F(AdaptationSetTest, GetRepresentations) { auto new_adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, kNoLanguage); - const uint64_t kPresentationTimeOffset = 80; Representation* new_representation2 = - new_adaptation_set->CopyRepresentationWithTimeOffset( - *representation2, kPresentationTimeOffset); + new_adaptation_set->CopyRepresentation(*representation2); Representation* new_representation1 = - new_adaptation_set->CopyRepresentationWithTimeOffset( - *representation1, kPresentationTimeOffset); + new_adaptation_set->CopyRepresentation(*representation1); EXPECT_THAT(new_adaptation_set->GetRepresentations(), // Elements are ordered by id(). diff --git a/packager/mpd/base/mock_mpd_builder.h b/packager/mpd/base/mock_mpd_builder.h index e8ffa91620b..d51d50d51d6 100644 --- a/packager/mpd/base/mock_mpd_builder.h +++ b/packager/mpd/base/mock_mpd_builder.h @@ -48,9 +48,8 @@ class MockAdaptationSet : public AdaptationSet { ~MockAdaptationSet() override; MOCK_METHOD1(AddRepresentation, Representation*(const MediaInfo& media_info)); - MOCK_METHOD2(CopyRepresentationWithTimeOffset, - Representation*(const Representation& representation, - uint64_t presentation_time_offset)); + MOCK_METHOD1(CopyRepresentation, + Representation*(const Representation& representation)); MOCK_METHOD1(AddContentProtectionElement, void(const ContentProtectionElement& element)); MOCK_METHOD2(UpdateContentProtectionPssh, diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index d0a28ce9ce7..b1fc1656d70 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -10,6 +10,7 @@ #include "packager/base/files/file_path.h" #include "packager/base/logging.h" +#include "packager/base/optional.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/stringprintf.h" #include "packager/base/synchronization/lock.h" @@ -172,23 +173,18 @@ xmlDocPtr MpdBuilder::GenerateMpd() { return nullptr; } - // Prefer Period@duration to Period@start for static MPD with more than one - // periods. - if (mpd_options_.mpd_type == MpdType::kStatic && periods_.size() > 1) { - // The duration of every period is determined by its start_time and next - // period start_time. The code below traverses |periods_| backwards. - double next_period_start_time = GetStaticMpdDuration(); - std::for_each( - periods_.rbegin(), periods_.rend(), - [&next_period_start_time](const std::unique_ptr& period) { - period->set_duration_seconds(next_period_start_time - - period->start_time_in_seconds()); - next_period_start_time = period->start_time_in_seconds(); - }); + bool output_period_duration = false; + if (mpd_options_.mpd_type == MpdType::kStatic) { + UpdatePeriodDurationAndPresentationTimestamp(); + // Only output period duration if there are more than one period. In the + // case of only one period, Period@duration is redundant as it is identical + // to Mpd Duration so the convention is not to output Period@duration. + output_period_duration = periods_.size() > 1; } for (const auto& period : periods_) { - xml::scoped_xml_ptr period_node(period->GetXml()); + xml::scoped_xml_ptr period_node( + period->GetXml(output_period_duration)); if (!period_node || !mpd.AddChild(std::move(period_node))) return nullptr; } @@ -309,26 +305,11 @@ void MpdBuilder::AddDynamicMpdInfo(XmlNode* mpd_node) { float MpdBuilder::GetStaticMpdDuration() { DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type); - if (periods_.empty()) { - LOG(WARNING) << "No Period found. Set MPD duration to 0."; - return 0.0f; - } - - // Attribute mediaPresentationDuration must be present for 'static' MPD. So - // setting "PT0S" is required even if none of the representaions have duration - // attribute. - float max_duration = 0.0f; - - // TODO(kqyang): Right now all periods contain the duration for the whole MPD. - // Simply get the duration from the first period. Ideally the period duration - // should only count the (sub)segments in that period. - for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) { - for (const auto* representation : adaptation_set->GetRepresentations()) { - max_duration = - std::max(representation->GetDurationSeconds(), max_duration); - } + float total_duration = 0.0f; + for (const auto& period : periods_) { + total_duration += period->duration_seconds(); } - return max_duration; + return total_duration; } bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) { @@ -336,10 +317,13 @@ bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) { DCHECK(!periods_.empty()); double timestamp = 0; double earliest_timestamp = -1; + // TODO(kqyang): This is used to set availabilityStartTime. We may consider + // set presentationTimeOffset in the Representations then we can set + // availabilityStartTime to the time when MPD is first generated. // The first period should have the earliest timestamp. for (const auto* adaptation_set : periods_.front()->GetAdaptationSets()) { for (const auto* representation : adaptation_set->GetRepresentations()) { - if (representation->GetEarliestTimestamp(×tamp) && + if (representation->GetStartAndEndTimestamps(×tamp, nullptr) && (earliest_timestamp < 0 || timestamp < earliest_timestamp)) { earliest_timestamp = timestamp; } @@ -351,6 +335,72 @@ bool MpdBuilder::GetEarliestTimestamp(double* timestamp_seconds) { return true; } +void MpdBuilder::UpdatePeriodDurationAndPresentationTimestamp() { + DCHECK_EQ(MpdType::kStatic, mpd_options_.mpd_type); + + bool first_period = true; + for (const auto& period : periods_) { + std::list video_representations; + std::list non_video_representations; + for (const auto& adaptation_set : period->GetAdaptationSets()) { + const auto& representations = adaptation_set->GetRepresentations(); + if (adaptation_set->IsVideo()) { + video_representations.insert(video_representations.end(), + representations.begin(), + representations.end()); + } else { + non_video_representations.insert(non_video_representations.end(), + representations.begin(), + representations.end()); + } + } + + base::Optional earliest_start_time; + base::Optional latest_end_time; + // The timestamps are based on Video Representations if exist. + const auto& representations = video_representations.size() > 0 + ? video_representations + : non_video_representations; + for (const auto& representation : representations) { + double start_time = 0; + double end_time = 0; + if (representation->GetStartAndEndTimestamps(&start_time, &end_time)) { + earliest_start_time = + std::min(earliest_start_time.value_or(start_time), start_time); + latest_end_time = + std::max(latest_end_time.value_or(end_time), end_time); + } + } + + if (!earliest_start_time) + return; + + period->set_duration_seconds(*latest_end_time - *earliest_start_time); + + double presentation_time_offset = *earliest_start_time; + if (first_period) { + first_period = false; + // Chrome does not like negative dts (https://crbug.com/398141). + // Always set presentationTimeOffset (pto) to 0 for the first period as it + // may result in an error on Chrome v63.0.3239.132 if it sets to a non + // zero value. + // It is fine with subsequent periods as the actual offset applied takes + // Period@start into consideration: + // offset = Period@start - presentationTimeOffset + // The result timestamp with offset applied is close to Period@start, so + // it is unlikely to result in a negative dts value. + // TODO(kqyang): Set the pto to |dts| instead of always setting it to 0 to + // workaround Chrome negative DTS bug. + presentation_time_offset = 0; + } + for (const auto& adaptation_set : period->GetAdaptationSets()) { + for (const auto& representation : adaptation_set->GetRepresentations()) { + representation->SetPresentationTimeOffset(presentation_time_offset); + } + } + } +} + void MpdBuilder::MakePathsRelativeToMpd(const std::string& mpd_path, MediaInfo* media_info) { DCHECK(media_info); diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index fa301d424ef..acbc0ce89c0 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -110,6 +110,9 @@ class MpdBuilder { // successful, false otherwise. bool GetEarliestTimestamp(double* timestamp_seconds); + // Update Period durations and presentation timestamps. + void UpdatePeriodDurationAndPresentationTimestamp(); + MpdOptions mpd_options_; std::list> periods_; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index 35da48f45e2..288354025c9 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -4,15 +4,19 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +#include #include #include #include "packager/mpd/base/adaptation_set.h" #include "packager/mpd/base/mpd_builder.h" #include "packager/mpd/base/period.h" +#include "packager/mpd/base/representation.h" #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/version/version.h" +DECLARE_int32(pto_adjustment); + using ::testing::HasSubstr; namespace shaka { @@ -56,6 +60,23 @@ class MpdBuilderTest : public ::testing::Test { ExpectMpdToEqualExpectedOutputFile(mpd_doc, expected_output_file)); } + void AddSegmentToPeriod(double segment_start_time_seconds, + double segment_duration_seconds, + Period* period) { + MediaInfo media_info = GetTestMediaInfo(kFileNameVideoMediaInfo1); + // Not relevant in this test. + const bool kContentProtectionFlag = true; + const size_t kBytes = 1000; + + AdaptationSet* adaptation_set = + period->GetOrCreateAdaptationSet(media_info, kContentProtectionFlag); + Representation* representation = + adaptation_set->AddRepresentation(media_info); + representation->AddNewSegment( + segment_start_time_seconds * media_info.reference_time_scale(), + segment_duration_seconds * media_info.reference_time_scale(), kBytes); + } + protected: // Creates a new AdaptationSet and adds a Representation element using // |media_info|. @@ -174,31 +195,57 @@ TEST_F(OnDemandMpdBuilderTest, MultiplePeriodTest) { } TEST_F(OnDemandMpdBuilderTest, MultiplePeriodCheckXmlTest) { - const double kPeriodStartTimeSeconds = 0.0; - const double kPeriodStartTimeSeconds2 = 3.1; - const double kPeriodStartTimeSeconds3 = 8.0; - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds); - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds2); - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds3); + // Disable pto adjustment. + FLAGS_pto_adjustment = 0; + + const double kPeriod1StartTimeSeconds = 0.0; + const double kPeriod2StartTimeSeconds = 3.1; + const double kPeriod3StartTimeSeconds = 8.0; + + // Actual period duration is determined by the segments not by the period + // start time above, which only provides an anchor point. + const double kPeriod1SegmentStartSeconds = 0.2; + const double kPeriod1SegmentDurationSeconds = 3.0; + const double kPeriod2SegmentStartSeconds = 5.5; + const double kPeriod2SegmentDurationSeconds = 10.5; + const double kPeriod3SegmentStartSeconds = 1.5; + const double kPeriod3SegmentDurationSeconds = 10.0; + + Period* period = mpd_.GetOrCreatePeriod(kPeriod1StartTimeSeconds); + AddSegmentToPeriod(kPeriod1SegmentStartSeconds, + kPeriod1SegmentDurationSeconds, period); + + period = mpd_.GetOrCreatePeriod(kPeriod2StartTimeSeconds); + AddSegmentToPeriod(kPeriod2SegmentStartSeconds, + kPeriod2SegmentDurationSeconds, period); + + period = mpd_.GetOrCreatePeriod(kPeriod3StartTimeSeconds); + AddSegmentToPeriod(kPeriod3SegmentStartSeconds, + kPeriod3SegmentDurationSeconds, period); std::string mpd_doc; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); + EXPECT_THAT(mpd_doc, HasSubstr("\n")); + EXPECT_THAT( + mpd_doc, + HasSubstr("")); + EXPECT_THAT(mpd_doc, HasSubstr("\n")); + EXPECT_THAT(mpd_doc, + HasSubstr("")); + EXPECT_THAT(mpd_doc, HasSubstr("\n")); EXPECT_THAT(mpd_doc, - HasSubstr(" \n" - " \n" - // There are no Representations so MPD duration is 0, - // which results in a negative duration for the last - // period. This would not happen in practice. - " \n")); + HasSubstr("")); } TEST_F(LiveMpdBuilderTest, MultiplePeriodCheckXmlTest) { - const double kPeriodStartTimeSeconds = 0.0; - const double kPeriodStartTimeSeconds2 = 3.1; - const double kPeriodStartTimeSeconds3 = 8.0; - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds); - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds2); - mpd_.GetOrCreatePeriod(kPeriodStartTimeSeconds3); + const double kPeriod1StartTimeSeconds = 0.0; + const double kPeriod2StartTimeSeconds = 3.1; + const double kPeriod3StartTimeSeconds = 8.0; + mpd_.GetOrCreatePeriod(kPeriod1StartTimeSeconds); + mpd_.GetOrCreatePeriod(kPeriod2StartTimeSeconds); + mpd_.GetOrCreatePeriod(kPeriod3StartTimeSeconds); std::string mpd_doc; ASSERT_TRUE(mpd_.ToString(&mpd_doc)); diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc index 1d40afeb8e0..df6882ff2f9 100644 --- a/packager/mpd/base/period.cc +++ b/packager/mpd/base/period.cc @@ -94,7 +94,7 @@ AdaptationSet* Period::GetOrCreateAdaptationSet( return adaptation_set_ptr; } -xml::scoped_xml_ptr Period::GetXml() const { +xml::scoped_xml_ptr Period::GetXml(bool output_period_duration) const { xml::XmlNode period("Period"); // Required for 'dynamic' MPDs. @@ -106,11 +106,10 @@ xml::scoped_xml_ptr Period::GetXml() const { return nullptr; } - if (duration_seconds_ != 0) { + if (output_period_duration) { period.SetStringAttribute("duration", SecondsToXmlDuration(duration_seconds_)); - } else if (mpd_options_.mpd_type == MpdType::kDynamic || - start_time_in_seconds_ != 0) { + } else if (mpd_options_.mpd_type == MpdType::kDynamic) { period.SetStringAttribute("start", SecondsToXmlDuration(start_time_in_seconds_)); } diff --git a/packager/mpd/base/period.h b/packager/mpd/base/period.h index 04bea93a951..054113e0b17 100644 --- a/packager/mpd/base/period.h +++ b/packager/mpd/base/period.h @@ -48,7 +48,7 @@ class Period { /// Generates xml element with its child AdaptationSet elements. /// @return On success returns a non-NULL scoped_xml_ptr. Otherwise returns a /// NULL scoped_xml_ptr. - xml::scoped_xml_ptr GetXml() const; + xml::scoped_xml_ptr GetXml(bool output_period_duration) const; /// @return The list of AdaptationSets in this Period. const std::list GetAdaptationSets() const; @@ -56,6 +56,9 @@ class Period { /// @return The start time of this Period. double start_time_in_seconds() const { return start_time_in_seconds_; } + /// @return period duration in seconds. + double duration_seconds() const { return duration_seconds_; } + /// Set period duration. void set_duration_seconds(double duration_seconds) { duration_seconds_ = duration_seconds; diff --git a/packager/mpd/base/period_unittest.cc b/packager/mpd/base/period_unittest.cc index 649f6fbf148..509191f16a9 100644 --- a/packager/mpd/base/period_unittest.cc +++ b/packager/mpd/base/period_unittest.cc @@ -28,6 +28,7 @@ const uint32_t kDefaultPeriodId = 9u; const double kDefaultPeriodStartTime = 5.6; const uint32_t kDefaultAdaptationSetId = 0u; const uint32_t kTrickPlayAdaptationSetId = 1u; +const bool kOutputPeriodDuration = true; bool ElementEqual(const Element& lhs, const Element& rhs) { const bool all_equal_except_sublement_check = @@ -138,12 +139,13 @@ TEST_P(PeriodTest, GetXml) { content_protection_in_adaptation_set_)); const char kExpectedXml[] = - "" + "" // ContentType and Representation elements are populated after // Representation::Init() is called. " " ""; - EXPECT_THAT(testable_period_.GetXml().get(), XmlNodeEqual(kExpectedXml)); + EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration).get(), + XmlNodeEqual(kExpectedXml)); } TEST_P(PeriodTest, DynamicMpdGetXml) { @@ -174,7 +176,8 @@ TEST_P(PeriodTest, DynamicMpdGetXml) { // Representation::Init() is called. " " ""; - EXPECT_THAT(testable_period_.GetXml().get(), XmlNodeEqual(kExpectedXml)); + EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration).get(), + XmlNodeEqual(kExpectedXml)); } TEST_P(PeriodTest, SetDurationAndGetXml) { @@ -206,7 +209,16 @@ TEST_P(PeriodTest, SetDurationAndGetXml) { // Representation::Init() is called. " " ""; - EXPECT_THAT(testable_period_.GetXml().get(), XmlNodeEqual(kExpectedXml)); + EXPECT_THAT(testable_period_.GetXml(kOutputPeriodDuration).get(), + XmlNodeEqual(kExpectedXml)); + const char kExpectedXmlSuppressDuration[] = + "" + // ContentType and Representation elements are populated after + // Representation::Init() is called. + " " + ""; + EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration).get(), + XmlNodeEqual(kExpectedXmlSuppressDuration)); } // Verify ForceSetSegmentAlignment is called. diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index edd04ab4a31..08ad897f8d2 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -6,11 +6,24 @@ #include "packager/mpd/base/representation.h" +#include + #include "packager/base/logging.h" #include "packager/mpd/base/mpd_options.h" #include "packager/mpd/base/mpd_utils.h" #include "packager/mpd/base/xml/xml_node.h" +DEFINE_int32( + pto_adjustment, + -1, + "There could be rounding errors in MSE which could cut the first key frame " + "of the representation and thus cut all the frames until the next key " + "frame, which then leads to a big gap in presentation timeline which " + "stalls playback. A small back off may be necessary to compensate for the " + "possible rounding error. It should not cause any playback issues if it is " + "small enough. The workaround can be removed once the problem is handled " + "in all players."); + namespace shaka { namespace { @@ -117,7 +130,6 @@ Representation::Representation( Representation::Representation( const Representation& representation, - uint64_t presentation_time_offset, std::unique_ptr state_change_listener) : Representation(representation.media_info_, representation.mpd_options_, @@ -129,8 +141,6 @@ Representation::Representation( start_number_ = representation.start_number_; for (const SegmentInfo& segment_info : representation.segment_infos_) start_number_ += segment_info.repeat + 1; - - media_info_.set_presentation_time_offset(presentation_time_offset); } Representation::~Representation() {} @@ -300,21 +310,36 @@ void Representation::SuppressOnce(SuppressFlag flag) { output_suppression_flags_ |= flag; } -bool Representation::GetEarliestTimestamp(double* timestamp_seconds) const { - DCHECK(timestamp_seconds); +void Representation::SetPresentationTimeOffset( + double presentation_time_offset) { + uint64_t pto = presentation_time_offset * media_info_.reference_time_scale(); + if (pto <= 0) + return; + pto += FLAGS_pto_adjustment; + media_info_.set_presentation_time_offset(pto); +} +bool Representation::GetStartAndEndTimestamps( + double* start_timestamp_seconds, + double* end_timestamp_seconds) const { if (segment_infos_.empty()) return false; - *timestamp_seconds = static_cast(segment_infos_.begin()->start_time) / - GetTimeScale(media_info_); + if (start_timestamp_seconds) { + *start_timestamp_seconds = + static_cast(segment_infos_.begin()->start_time) / + GetTimeScale(media_info_); + } + if (end_timestamp_seconds) { + *end_timestamp_seconds = + static_cast(segment_infos_.rbegin()->start_time + + segment_infos_.rbegin()->duration * + (segment_infos_.rbegin()->repeat + 1)) / + GetTimeScale(media_info_); + } return true; } -float Representation::GetDurationSeconds() const { - return media_info_.media_duration_seconds(); -} - bool Representation::HasRequiredMediaInfoFields() const { if (HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_)) { LOG(ERROR) << "MediaInfo cannot have both VOD and Live fields."; diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index bc632ecc3b3..70ba85dd015 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -128,12 +128,19 @@ class Representation { /// This may be called multiple times to set different (or the same) flags. void SuppressOnce(SuppressFlag flag); - /// Gets the earliest, normalized segment timestamp. + /// Set @presentationTimeOffset in SegmentBase / SegmentTemplate. + void SetPresentationTimeOffset(double presentation_time_offset); + + /// Gets the start and end timestamps in seconds. + /// @param start_timestamp_seconds contains the returned start timestamp in + /// seconds on success. It can be nullptr, which means that start + /// timestamp does not need to be returned. + /// @param end_timestamp_seconds contains the returned end timestamp in + /// seconds on success. It can be nullptr, which means that end + /// timestamp does not need to be returned. /// @return true if successful, false otherwise. - bool GetEarliestTimestamp(double* timestamp_seconds) const; - - /// @return The duration of the Representation in seconds. - float GetDurationSeconds() const; + bool GetStartAndEndTimestamps(double* start_timestamp_seconds, + double* end_timestamp_seconds) const; /// @return ID number for . uint32_t id() const { return id_; } @@ -154,13 +161,10 @@ class Representation { std::unique_ptr state_change_listener); /// @param representation points to the original Representation to be cloned. - /// @param presentation_time_offset is the presentation time offset for the - /// new Representation. /// @param state_change_listener is an event handler for state changes to /// the representation. If null, no event handler registered. Representation( const Representation& representation, - uint64_t presentation_time_offset, std::unique_ptr state_change_listener); private: diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index 537a40f327c..045067cf6f6 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -6,6 +6,7 @@ #include "packager/mpd/base/representation.h" +#include #include #include #include @@ -15,6 +16,8 @@ #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" +DECLARE_int32(pto_adjustment); + using ::testing::Not; namespace shaka { @@ -54,12 +57,10 @@ class RepresentationTest : public ::testing::Test { std::unique_ptr CopyRepresentation( const Representation& representation, - uint64_t presentation_time_offset, std::unique_ptr state_change_listener) { return std::unique_ptr( - new Representation(representation, presentation_time_offset, - std::move(state_change_listener))); + new Representation(representation, std::move(state_change_listener))); } std::unique_ptr NoListener() { @@ -506,16 +507,14 @@ TEST_F(SegmentTemplateTest, RepresentationClone) { const uint64_t kSize = 128; AddSegments(kStartTime, kDuration, kSize, 0); - const uint64_t kPresentationTimeOffset = 100; - auto cloned_representation = CopyRepresentation( - *representation_, kPresentationTimeOffset, NoListener()); + auto cloned_representation = + CopyRepresentation(*representation_, NoListener()); const char kExpectedXml[] = "\n" - " \n" + " \n" " \n" " \n" "\n"; @@ -523,30 +522,50 @@ TEST_F(SegmentTemplateTest, RepresentationClone) { XmlNodeEqual(kExpectedXml)); } -TEST_F(SegmentTemplateTest, GetEarliestTimestamp) { - double earliest_timestamp; - // No segments. - EXPECT_FALSE(representation_->GetEarliestTimestamp(&earliest_timestamp)); +TEST_F(SegmentTemplateTest, PresentationTimeOffset) { + FLAGS_pto_adjustment = -1; - const uint64_t kStartTime = 88; + const uint64_t kStartTime = 0; const uint64_t kDuration = 10; const uint64_t kSize = 128; AddSegments(kStartTime, kDuration, kSize, 0); - AddSegments(kStartTime + kDuration, kDuration, kSize, 0); - ASSERT_TRUE(representation_->GetEarliestTimestamp(&earliest_timestamp)); - EXPECT_EQ(static_cast(kStartTime) / kDefaultTimeScale, - earliest_timestamp); + + const double kPresentationTimeOffsetSeconds = 2.3; + representation_->SetPresentationTimeOffset(kPresentationTimeOffsetSeconds); + + const char kExpectedXml[] = + "\n" + // pto = kPresentationTimeOffsetSeconds * timescale + FLAGS_pto_adjustment + " \n" + " \n" + " \n" + " \n" + " \n" + "\n"; + EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); } -TEST_F(SegmentTemplateTest, GetDuration) { - const float kMediaDurationSeconds = 88.8f; - MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); - media_info.set_media_duration_seconds(kMediaDurationSeconds); - representation_ = - CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); - ASSERT_TRUE(representation_->Init()); +TEST_F(SegmentTemplateTest, GetStartAndEndTimestamps) { + double start_timestamp; + double end_timestamp; + // No segments. + EXPECT_FALSE(representation_->GetStartAndEndTimestamps(&start_timestamp, + &end_timestamp)); - EXPECT_EQ(kMediaDurationSeconds, representation_->GetDurationSeconds()); + const uint64_t kStartTime = 88; + const uint64_t kDuration = 10; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + AddSegments(kStartTime + kDuration, kDuration, kSize, 2); + ASSERT_TRUE(representation_->GetStartAndEndTimestamps(&start_timestamp, + &end_timestamp)); + EXPECT_EQ(static_cast(kStartTime) / kDefaultTimeScale, + start_timestamp); + EXPECT_EQ(static_cast(kStartTime + kDuration * 4) / kDefaultTimeScale, + end_timestamp); } TEST_F(SegmentTemplateTest, NormalRepeatedSegmentDuration) { diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index 1449ad9e3dd..b15a6fd08b1 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -6,8 +6,6 @@ #include "packager/mpd/base/simple_mpd_notifier.h" -#include - #include "packager/base/logging.h" #include "packager/base/stl_util.h" #include "packager/mpd/base/adaptation_set.h" @@ -17,17 +15,6 @@ #include "packager/mpd/base/period.h" #include "packager/mpd/base/representation.h" -DEFINE_int32( - pto_adjustment, - -1, - "There could be rounding errors in MSE which could cut the first key frame " - "of the representation and thus cut all the frames until the next key " - "frame, which then leads to a big gap in presentation timeline which " - "stalls playback. A small back off may be necessary to compensate for the " - "possible rounding error. It should not cause any playback issues if it is " - "small enough. The workaround can be removed once the problem is handled " - "in all players."); - namespace shaka { SimpleMpdNotifier::SimpleMpdNotifier(const MpdOptions& mpd_options) @@ -164,13 +151,8 @@ Representation* SimpleMpdNotifier::AddRepresentationToPeriod( Representation* representation = nullptr; if (original_representation) { - uint64_t presentation_time_offset = - period->start_time_in_seconds() * media_info.reference_time_scale(); - if (presentation_time_offset > 0) { - presentation_time_offset += FLAGS_pto_adjustment; - } - representation = adaptation_set->CopyRepresentationWithTimeOffset( - *original_representation, presentation_time_offset); + representation = + adaptation_set->CopyRepresentation(*original_representation); } else { representation = adaptation_set->AddRepresentation(media_info); } diff --git a/packager/mpd/base/simple_mpd_notifier_unittest.cc b/packager/mpd/base/simple_mpd_notifier_unittest.cc index 3c212a44a31..a1724aa264a 100644 --- a/packager/mpd/base/simple_mpd_notifier_unittest.cc +++ b/packager/mpd/base/simple_mpd_notifier_unittest.cc @@ -4,7 +4,6 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd -#include #include #include #include @@ -17,8 +16,6 @@ #include "packager/mpd/base/simple_mpd_notifier.h" #include "packager/mpd/test/mpd_builder_test_helper.h" -DECLARE_int32(pto_adjustment); - namespace shaka { using ::testing::_; @@ -245,10 +242,8 @@ TEST_F(SimpleMpdNotifierTest, NotifyCueEvent) { EXPECT_CALL(*mock_period2, GetOrCreateAdaptationSet(EqualsProto(valid_media_info1_), _)) .WillOnce(Return(mock_adaptation_set2.get())); - EXPECT_CALL( - *mock_adaptation_set2, - CopyRepresentationWithTimeOffset( - Ref(*mock_representation), kCueEventTimestamp + FLAGS_pto_adjustment)) + EXPECT_CALL(*mock_adaptation_set2, + CopyRepresentation(Ref(*mock_representation))) .WillOnce(Return(mock_representation2.get())); EXPECT_TRUE(notifier.NotifyCueEvent(container_id, kCueEventTimestamp)); } diff --git a/packager/mpd/test/data/audio_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/audio_media_info1_expected_mpd_output.txt index 3a645f65051..b9b235f4665 100644 --- a/packager/mpd/test/data/audio_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/audio_media_info1_expected_mpd_output.txt @@ -1,5 +1,5 @@ - + diff --git a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt index ce0e0fc1c68..fe1ba62c4f0 100644 --- a/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/audio_media_info1_video_media_info1_expected_mpd_output.txt @@ -1,5 +1,5 @@ - + diff --git a/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt b/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt index 5ff5c5441fd..22014879001 100644 --- a/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt +++ b/packager/mpd/test/data/video_media_info1_expected_mpd_output.txt @@ -1,5 +1,5 @@ - + diff --git a/packager/mpd/test/data/video_media_info1and2_expected_mpd_output.txt b/packager/mpd/test/data/video_media_info1and2_expected_mpd_output.txt index 8e6cc030ece..bd6b978a509 100644 --- a/packager/mpd/test/data/video_media_info1and2_expected_mpd_output.txt +++ b/packager/mpd/test/data/video_media_info1and2_expected_mpd_output.txt @@ -1,5 +1,5 @@ - +