From 224b597b482c1f9bb329bf26104672703ea75e21 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Mon, 25 Sep 2017 12:18:50 -0700 Subject: [PATCH] Support approximate SegmentTimeline It is not always possible to align segment duration to target duration exactly. For example, for AAC with sampling rate of 44100, there are always 1024 audio frames per sample, so the sample duration is 1024/44100. For a target duration of 2 seconds, the closest segment duration would be 1.984 or 2.00533. This feature allows MPD generator to treat these segments as having the same duration, thus allows MPD generator to generate less SegmentTimeline entries and potentially no SegmentTimeline entries (replaced with SegmentTemplate@duration instead if --segment_template_constant_duration flag is enabled). Under flag --allow_approximate_segment_timeline. Disabled by default. Fixes #330. Change-Id: I5044eaa348ebbf45bf792a2af53fc95a115ae21b --- docs/source/options/dash_options.rst | 14 + packager/app/mpd_flags.cc | 12 + packager/app/mpd_flags.h | 1 + packager/app/packager_main.cc | 2 + packager/app/packager_util.cc | 5 +- packager/app/packager_util.h | 4 +- packager/mpd/base/media_info.proto | 2 +- packager/mpd/base/mpd_options.h | 5 + packager/mpd/base/mpd_utils.cc | 3 +- packager/mpd/base/representation.cc | 146 +++++---- packager/mpd/base/representation.h | 28 +- packager/mpd/base/representation_unittest.cc | 293 +++++++++++++++---- packager/mpd/base/xml/xml_node.cc | 68 ++++- packager/mpd/base/xml/xml_node_unittest.cc | 171 +++++++++++ packager/mpd/mpd.gyp | 1 + packager/mpd/public/mpd_params.h | 10 + packager/packager.cc | 6 +- 17 files changed, 631 insertions(+), 140 deletions(-) diff --git a/docs/source/options/dash_options.rst b/docs/source/options/dash_options.rst index a36787aefd..ee48eaad43 100644 --- a/docs/source/options/dash_options.rst +++ b/docs/source/options/dash_options.rst @@ -48,3 +48,17 @@ DASH options Any audio/text tracks tagged with this language will have in the manifest. This allows the player to choose the correct default language for the content. + +--allow_approximate_segment_timeline + + For live profile only. + + If enabled, segments with close duration (i.e. with difference less than + one sample) are considered to have the same duration. This enables + MPD generator to generate less SegmentTimeline entries. If all segments + are of the same duration except the last one, we will do further + optimization to use SegmentTemplate@duration instead and omit + SegmentTimeline completely. + + Ignored if $Time$ is used in segment template, since $Time$ requires + accurate Segment Timeline. diff --git a/packager/app/mpd_flags.cc b/packager/app/mpd_flags.cc index 01a260d217..9d51435df9 100644 --- a/packager/app/mpd_flags.cc +++ b/packager/app/mpd_flags.cc @@ -51,3 +51,15 @@ DEFINE_bool(generate_dash_if_iop_compliant_mpd, true, "Try to generate DASH-IF IOP compliant MPD. This is best effort " "and does not guarantee compliance."); +DEFINE_bool( + allow_approximate_segment_timeline, + false, + "For live profile only. " + "If enabled, segments with close duration (i.e. with difference less than " + "one sample) are considered to have the same duration. This enables MPD " + "generator to generate less SegmentTimeline entries. If all segments are " + "of the same duration except the last one, we will do further optimization " + "to use SegmentTemplate@duration instead and omit SegmentTimeline " + "completely." + "Ignored if $Time$ is used in segment template, since $Time$ requires " + "accurate Segment Timeline."); diff --git a/packager/app/mpd_flags.h b/packager/app/mpd_flags.h index 8377df47ab..5675f65d6c 100644 --- a/packager/app/mpd_flags.h +++ b/packager/app/mpd_flags.h @@ -20,5 +20,6 @@ DECLARE_double(min_buffer_time); DECLARE_double(suggested_presentation_delay); DECLARE_string(utc_timings); DECLARE_bool(generate_dash_if_iop_compliant_mpd); +DECLARE_bool(allow_approximate_segment_timeline); #endif // APP_MPD_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 9a12916cc1..ce762dfa51 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -420,6 +420,8 @@ base::Optional GetPackagingParams() { mpd_params.generate_static_live_mpd = FLAGS_generate_static_mpd; mpd_params.generate_dash_if_iop_compliant_mpd = FLAGS_generate_dash_if_iop_compliant_mpd; + mpd_params.allow_approximate_segment_timeline = + FLAGS_allow_approximate_segment_timeline; HlsParams& hls_params = packaging_params.hls_params; if (!GetHlsPlaylistType(FLAGS_hls_playlist_type, &hls_params.playlist_type)) { diff --git a/packager/app/packager_util.cc b/packager/app/packager_util.cc index cf67e3ccb4..b40239b8ce 100644 --- a/packager/app/packager_util.cc +++ b/packager/app/packager_util.cc @@ -181,7 +181,9 @@ std::unique_ptr CreateDecryptionKeySource( return decryption_key_source; } -MpdOptions GetMpdOptions(bool on_demand_profile, const MpdParams& mpd_params) { +MpdOptions GetMpdOptions(bool on_demand_profile, + const MpdParams& mpd_params, + double target_segment_duration) { MpdOptions mpd_options; mpd_options.dash_profile = on_demand_profile ? DashProfile::kOnDemand : DashProfile::kLive; @@ -190,6 +192,7 @@ MpdOptions GetMpdOptions(bool on_demand_profile, const MpdParams& mpd_params) { ? MpdType::kStatic : MpdType::kDynamic; mpd_options.mpd_params = mpd_params; + mpd_options.target_segment_duration = target_segment_duration; return mpd_options; } diff --git a/packager/app/packager_util.h b/packager/app/packager_util.h index c5351545de..c47c624db0 100644 --- a/packager/app/packager_util.h +++ b/packager/app/packager_util.h @@ -45,7 +45,9 @@ std::unique_ptr CreateDecryptionKeySource( const DecryptionParams& decryption_params); /// @return MpdOptions from provided inputs. -MpdOptions GetMpdOptions(bool on_demand_profile, const MpdParams& mpd_params); +MpdOptions GetMpdOptions(bool on_demand_profile, + const MpdParams& mpd_params, + double target_segment_duration); } // namespace media } // namespace shaka diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 721f7ee377..6555306b94 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -151,7 +151,7 @@ message MediaInfo { // This value is the user input “segment duration”. // This value is not necessarily the same as the value passed to // MpdNotifier::NotifyNewSegment(). - optional float segment_duration_seconds = 12; + optional float segment_duration_seconds = 12 [deprecated = true]; // END LIVE only. // URL fields for the corresponding file_name fields above. diff --git a/packager/mpd/base/mpd_options.h b/packager/mpd/base/mpd_options.h index dc261d9718..f44dc83f1e 100644 --- a/packager/mpd/base/mpd_options.h +++ b/packager/mpd/base/mpd_options.h @@ -26,6 +26,11 @@ struct MpdOptions { DashProfile dash_profile = DashProfile::kOnDemand; MpdType mpd_type = MpdType::kStatic; MpdParams mpd_params; + /// This is the target segment duration requested by the user. The actual + /// segment duration may be different to the target segment duration. + /// This parameter is included here to calculate the approximate + /// SegmentTimeline if it is enabled. + double target_segment_duration = 0; }; } // namespace shaka diff --git a/packager/mpd/base/mpd_utils.cc b/packager/mpd/base/mpd_utils.cc index 49d51556ad..08fc5dc095 100644 --- a/packager/mpd/base/mpd_utils.cc +++ b/packager/mpd/base/mpd_utils.cc @@ -58,8 +58,7 @@ bool HasVODOnlyFields(const MediaInfo& media_info) { bool HasLiveOnlyFields(const MediaInfo& media_info) { return media_info.has_init_segment_url() || - media_info.has_segment_template_url() || - media_info.has_segment_duration_seconds(); + media_info.has_segment_template_url(); } void RemoveDuplicateAttributes( diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index 716f1667bb..ebee94b55c 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -8,6 +8,8 @@ #include +#include + #include "packager/base/logging.h" #include "packager/file/file.h" #include "packager/media/base/muxer_util.h" @@ -115,9 +117,12 @@ Representation::Representation( id_(id), bandwidth_estimator_(BandwidthEstimator::kUseAllBlocks), mpd_options_(mpd_options), - start_number_(1), state_change_listener_(std::move(state_change_listener)), - output_suppression_flags_(0) {} + allow_approximate_segment_timeline_( + // TODO(kqyang): Need a better check. $Time is legitimate but not a + // template. + media_info.segment_template().find("$Time") == std::string::npos && + mpd_options_.mpd_params.allow_approximate_segment_timeline) {} Representation::Representation( const Representation& representation, @@ -200,12 +205,8 @@ void Representation::AddNewSegment(uint64_t start_time, if (state_change_listener_) state_change_listener_->OnNewSegmentForRepresentation(start_time, duration); - if (IsContiguous(start_time, duration, size)) { - ++segment_infos_.back().repeat; - } else { - SegmentInfo s = {start_time, duration, /* Not repeat. */ 0}; - segment_infos_.push_back(s); - } + + AddSegmentInfo(start_time, duration); bandwidth_estimator_.AddBlock( size, static_cast(duration) / media_info_.reference_time_scale()); @@ -214,12 +215,17 @@ void Representation::AddNewSegment(uint64_t start_time, DCHECK_GE(segment_infos_.size(), 1u); } -void Representation::SetSampleDuration(uint32_t sample_duration) { +void Representation::SetSampleDuration(uint32_t frame_duration) { + // Sample duration is used to generate approximate SegmentTimeline. + // Text is required to have exactly the same segment duration. + if (media_info_.has_audio_info() || media_info_.has_video_info()) + frame_duration_ = frame_duration; + if (media_info_.has_video_info()) { - media_info_.mutable_video_info()->set_frame_duration(sample_duration); + media_info_.mutable_video_info()->set_frame_duration(frame_duration); if (state_change_listener_) { state_change_listener_->OnSetFrameRateForRepresentation( - sample_duration, media_info_.video_info().time_scale()); + frame_duration, media_info_.video_info().time_scale()); } } } @@ -344,55 +350,87 @@ bool Representation::HasRequiredMediaInfoFields() const { return true; } -bool Representation::IsContiguous(uint64_t start_time, - uint64_t duration, - uint64_t size) const { - if (segment_infos_.empty()) - return false; +void Representation::AddSegmentInfo(uint64_t start_time, uint64_t duration) { + const uint64_t kNoRepeat = 0; + const uint64_t adjusted_duration = AdjustDuration(duration); - // Contiguous segment. - const SegmentInfo& previous = segment_infos_.back(); - const uint64_t previous_segment_end_time = - previous.start_time + previous.duration * (previous.repeat + 1); - if (previous_segment_end_time == start_time && - segment_infos_.back().duration == duration) { - return true; + if (!segment_infos_.empty()) { + // Contiguous segment. + const SegmentInfo& previous = segment_infos_.back(); + const uint64_t previous_segment_end_time = + previous.start_time + previous.duration * (previous.repeat + 1); + // Make it continuous if the segment start time is close to previous segment + // end time. + if (ApproximiatelyEqual(previous_segment_end_time, start_time)) { + const uint64_t segment_end_time_for_same_duration = + previous_segment_end_time + previous.duration; + const uint64_t actual_segment_end_time = start_time + duration; + // Consider the segments having identical duration if the segment end time + // is close to calculated segment end time by assuming identical duration. + if (ApproximiatelyEqual(segment_end_time_for_same_duration, + actual_segment_end_time)) { + ++segment_infos_.back().repeat; + } else { + segment_infos_.push_back( + {previous_segment_end_time, + actual_segment_end_time - previous_segment_end_time, kNoRepeat}); + } + return; + } + + // A gap since previous. + const uint64_t kRoundingErrorGrace = 5; + if (previous_segment_end_time + kRoundingErrorGrace < start_time) { + LOG(WARNING) << "Found a gap of size " + << (start_time - previous_segment_end_time) + << " > kRoundingErrorGrace (" << kRoundingErrorGrace + << "). The new segment starts at " << start_time + << " but the previous segment ends at " + << previous_segment_end_time << "."; + } + + // No overlapping segments. + if (start_time < previous_segment_end_time - kRoundingErrorGrace) { + LOG(WARNING) + << "Segments should not be overlapping. The new segment starts at " + << start_time << " but the previous segment ends at " + << previous_segment_end_time << "."; + } } - // No out of order segments. - const uint64_t previous_segment_start_time = - previous.start_time + previous.duration * previous.repeat; - if (previous_segment_start_time >= start_time) { - LOG(ERROR) << "Segments should not be out of order segment. Adding segment " - "with start_time == " - << start_time << " but the previous segment starts at " - << previous_segment_start_time << "."; - return false; - } + segment_infos_.push_back({start_time, adjusted_duration, kNoRepeat}); +} - // A gap since previous. - const uint64_t kRoundingErrorGrace = 5; - if (previous_segment_end_time + kRoundingErrorGrace < start_time) { - LOG(WARNING) << "Found a gap of size " - << (start_time - previous_segment_end_time) - << " > kRoundingErrorGrace (" << kRoundingErrorGrace - << "). The new segment starts at " << start_time - << " but the previous segment ends at " - << previous_segment_end_time << "."; - return false; - } +bool Representation::ApproximiatelyEqual(uint64_t time1, uint64_t time2) const { + if (!allow_approximate_segment_timeline_) + return time1 == time2; - // No overlapping segments. - if (start_time < previous_segment_end_time - kRoundingErrorGrace) { - LOG(WARNING) - << "Segments should not be overlapping. The new segment starts at " - << start_time << " but the previous segment ends at " - << previous_segment_end_time << "."; - return false; - } + // It is not always possible to align segment duration to target duration + // exactly. For example, for AAC with sampling rate of 44100, there are always + // 1024 audio samples per frame, so the frame duration is 1024/44100. For a + // target duration of 2 seconds, the closest segment duration would be 1.984 + // or 2.00533. - // Within rounding error grace but technically not contiguous in terms of MPD. - return false; + // An arbitrary error threshold cap. This makes sure that the error is not too + // large for large samples. + const double kErrorThresholdSeconds = 0.05; + + // So we consider two times equal if they differ by less than one sample. + const uint32_t error_threshold = + std::min(frame_duration_, + static_cast(kErrorThresholdSeconds * + media_info_.reference_time_scale())); + return time1 < time2 + error_threshold && time2 < time1 + error_threshold; +} + +uint64_t Representation::AdjustDuration(uint64_t duration) const { + if (!allow_approximate_segment_timeline_) + return duration; + const uint64_t scaled_target_duration = + mpd_options_.target_segment_duration * media_info_.reference_time_scale(); + return ApproximiatelyEqual(scaled_target_duration, duration) + ? scaled_target_duration + : duration; } void Representation::SlideWindow() { diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index 6b8899b0d7..0cdd8f7a75 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -178,11 +178,18 @@ class Representation { // Representation. Otherwise returns false. bool HasRequiredMediaInfoFields() const; - // Return false if the segment should be considered a new segment. True if the - // segment is contiguous. - bool IsContiguous(uint64_t start_time, - uint64_t duration, - uint64_t size) const; + // Add a SegmentInfo. This function may insert an adjusted SegmentInfo if + // |allow_approximate_segment_timeline_| is set. + void AddSegmentInfo(uint64_t start_time, uint64_t duration); + + // Check if two timestamps are approximately equal if + // |allow_approximate_segment_timeline_| is set; Otherwise check whether the + // two times match. + bool ApproximiatelyEqual(uint64_t time1, uint64_t time2) const; + + // Return adjusted duration if |allow_aproximate_segment_timeline_or_duration| + // is set; otherwise duration is returned without adjustment. + uint64_t AdjustDuration(uint64_t duration) const; // Remove elements from |segment_infos_| for dynamic live profile. Increments // |start_number_| by the number of segments removed. @@ -216,14 +223,21 @@ class Representation { // startNumber attribute for SegmentTemplate. // Starts from 1. - uint32_t start_number_; + uint32_t start_number_ = 1; // If this is not null, then Representation is responsible for calling the // right methods at right timings. std::unique_ptr state_change_listener_; // Bit vector for tracking witch attributes should not be output. - int output_suppression_flags_; + int output_suppression_flags_ = 0; + + // When set to true, allows segments to have slightly different durations (up + // to one sample). + const bool allow_approximate_segment_timeline_ = false; + // Segments with duration difference less than one frame duration are + // considered to have the same duration. + uint32_t frame_duration_ = 0; }; } // namespace shaka diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index ca1a2d47c9..16b5cb0785 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -17,7 +17,10 @@ #include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" +using ::testing::Bool; using ::testing::Not; +using ::testing::Values; +using ::testing::WithParamInterface; namespace shaka { namespace { @@ -395,6 +398,8 @@ const char kSElementTemplateWithoutR[] = "\n"; const int kDefaultStartNumber = 1; const uint32_t kDefaultTimeScale = 1000u; +const uint64_t kScaledTargetSegmentDuration = 10; +const uint32_t kSampleDuration = 2; std::string GetDefaultMediaInfo() { const char kMediaInfo[] = @@ -487,18 +492,8 @@ TEST_F(SegmentTemplateTest, OneSegmentNormal) { const uint64_t kSize = 128; AddSegments(kStartTime, kDuration, kSize, 0); - const char kExpectedXml[] = - "\n" - " \n" - " \n" - " \n" - " \n" - " \n" - "\n"; - EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); + expected_s_elements_ = ""; + EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(ExpectedXml())); } TEST_F(SegmentTemplateTest, RepresentationClone) { @@ -521,7 +516,6 @@ TEST_F(SegmentTemplateTest, RepresentationClone) { " width=\"720\" height=\"480\" frameRate=\"10/5\">\n" " \n" - " \n" " \n" "\n"; EXPECT_THAT(cloned_representation->GetXml().get(), @@ -545,7 +539,7 @@ TEST_F(SegmentTemplateTest, PresentationTimeOffset) { " initialization=\"init.mp4\" media=\"$Time$.mp4\" startNumber=\"1\">\n" " \n" " \n" - " \n" + " \n" " \n" "\n"; EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml)); @@ -686,7 +680,7 @@ TEST_F(SegmentTemplateTest, OverlappingSegmentsWithinErrorRange) { EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(ExpectedXml())); } -class TimeShiftBufferDepthTest : public SegmentTemplateTest { +class SegmentTimelineTestBase : public SegmentTemplateTest { public: void SetUp() override { // The only diff with current GetDefaultMediaInfo() is that this uses @@ -708,6 +702,8 @@ class TimeShiftBufferDepthTest : public SegmentTemplateTest { const std::string& number_template_media_info = base::StringPrintf(kMediaInfo, kDefaultTimeScale); mpd_options_.mpd_type = MpdType::kDynamic; + mpd_options_.target_segment_duration = + static_cast(kScaledTargetSegmentDuration) / kDefaultTimeScale; representation_ = CreateRepresentation(ConvertToMediaInfo(number_template_media_info), kAnyRepresentationId, NoListener()); @@ -734,17 +730,198 @@ class TimeShiftBufferDepthTest : public SegmentTemplateTest { expected_start_number, expected_s_element.c_str()); } +}; + +class ApproximateSegmentTimelineTest : public SegmentTimelineTestBase, + public WithParamInterface { + public: + void SetUp() override { + allow_approximate_segment_timeline_ = GetParam(); + mpd_options_.mpd_params.allow_approximate_segment_timeline = + allow_approximate_segment_timeline_; + SegmentTimelineTestBase::SetUp(); + representation_->SetSampleDuration(kSampleDuration); + } + + std::string ExpectedXml(const std::string& expected_s_element) { + return SegmentTimelineTestBase::ExpectedXml(expected_s_element, + kDefaultStartNumber); + } + + protected: + bool allow_approximate_segment_timeline_; +}; + +TEST_P(ApproximateSegmentTimelineTest, SegmentDurationAdjusted) { + const uint64_t kStartTime = 0; + const uint64_t kDurationSmaller = + kScaledTargetSegmentDuration - kSampleDuration / 2; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDurationSmaller, kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + expected_s_elements = base::StringPrintf( + kSElementTemplateWithoutR, kStartTime, kScaledTargetSegmentDuration); + } else { + expected_s_elements = base::StringPrintf(kSElementTemplateWithoutR, + kStartTime, kDurationSmaller); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +TEST_P(ApproximateSegmentTimelineTest, + SegmentDurationAdjustedWithNonZeroStartTime) { + const uint64_t kStartTime = 12345; + const uint64_t kDurationSmaller = + kScaledTargetSegmentDuration - kSampleDuration / 2; + const uint64_t kSize = 128; + + AddSegments(kStartTime, kDurationSmaller, kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + expected_s_elements = base::StringPrintf( + kSElementTemplateWithoutR, kStartTime, kScaledTargetSegmentDuration); + } else { + expected_s_elements = base::StringPrintf(kSElementTemplateWithoutR, + kStartTime, kDurationSmaller); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +TEST_P(ApproximateSegmentTimelineTest, SegmentsWithSimilarDurations) { + const uint64_t kStartTime = 0; + const uint64_t kDurationSmaller = + kScaledTargetSegmentDuration - kSampleDuration / 2; + const uint64_t kDurationLarger = + kScaledTargetSegmentDuration + kSampleDuration / 2; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDurationSmaller, kSize, 0); + AddSegments(kStartTime + kDurationSmaller, kDurationLarger, kSize, 0); + AddSegments(kStartTime + kDurationSmaller + kDurationLarger, kDurationSmaller, + kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + uint64_t kNumSegments = 3; + expected_s_elements = + base::StringPrintf(kSElementTemplate, kStartTime, + kScaledTargetSegmentDuration, kNumSegments - 1); + } else { + expected_s_elements = + base::StringPrintf(kSElementTemplateWithoutR, kStartTime, + kDurationSmaller) + + base::StringPrintf(kSElementTemplateWithoutR, + kStartTime + kDurationSmaller, kDurationLarger) + + base::StringPrintf(kSElementTemplateWithoutR, + kStartTime + kDurationSmaller + kDurationLarger, + kDurationSmaller); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +// We assume the actual segment duration fluctuates around target segment +// duration; if it is not the case (which should not happen with our demuxer), +// this is how the output would look like. +TEST_P(ApproximateSegmentTimelineTest, SegmentsWithSimilarDurations2) { + const uint64_t kStartTime = 0; + const uint64_t kDurationLarger = + kScaledTargetSegmentDuration + kSampleDuration / 2; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDurationLarger, kSize, 0); + AddSegments(kStartTime + kDurationLarger, kDurationLarger, kSize, 0); + AddSegments(kStartTime + 2 * kDurationLarger, kDurationLarger, kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + expected_s_elements = + "" + ""; + } else { + uint64_t kNumSegments = 3; + expected_s_elements = base::StringPrintf(kSElementTemplate, kStartTime, + kDurationLarger, kNumSegments - 1); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +TEST_P(ApproximateSegmentTimelineTest, FillSmallGap) { + const uint64_t kStartTime = 0; + const uint64_t kDuration = kScaledTargetSegmentDuration; + const uint64_t kGap = kSampleDuration / 2; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + AddSegments(kStartTime + kDuration + kGap, kDuration, kSize, 0); + AddSegments(kStartTime + 2 * kDuration + kGap, kDuration, kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + uint64_t kNumSegments = 3; + expected_s_elements = base::StringPrintf(kSElementTemplate, kStartTime, + kDuration, kNumSegments - 1); + } else { + expected_s_elements = + base::StringPrintf(kSElementTemplateWithoutR, kStartTime, kDuration) + + base::StringPrintf(kSElementTemplate, kStartTime + kDuration + kGap, + kDuration, static_cast(1) /* repeat */); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +TEST_P(ApproximateSegmentTimelineTest, FillSmallOverlap) { + const uint64_t kStartTime = 0; + const uint64_t kDuration = kScaledTargetSegmentDuration; + const uint64_t kOverlap = kSampleDuration / 2; + const uint64_t kSize = 128; + AddSegments(kStartTime, kDuration, kSize, 0); + AddSegments(kStartTime + kDuration - kOverlap, kDuration, kSize, 0); + AddSegments(kStartTime + 2 * kDuration - kOverlap, kDuration, kSize, 0); + + std::string expected_s_elements; + if (allow_approximate_segment_timeline_) { + uint64_t kNumSegments = 3; + expected_s_elements = base::StringPrintf(kSElementTemplate, kStartTime, + kDuration, kNumSegments - 1); + } else { + expected_s_elements = + base::StringPrintf(kSElementTemplateWithoutR, kStartTime, kDuration) + + base::StringPrintf(kSElementTemplate, kStartTime + kDuration - kOverlap, + kDuration, static_cast(1) /* repeat */); + } + EXPECT_THAT(representation_->GetXml().get(), + XmlNodeEqual(ExpectedXml(expected_s_elements))); +} + +INSTANTIATE_TEST_CASE_P(ApproximateSegmentTimelineTest, + ApproximateSegmentTimelineTest, + Bool()); + +class TimeShiftBufferDepthTest : public SegmentTimelineTestBase, + public WithParamInterface { + public: + void SetUp() override { + initial_start_time_ = GetParam(); + SegmentTimelineTestBase::SetUp(); + } MpdOptions* mutable_mpd_options() { return &mpd_options_; } + + protected: + uint64_t initial_start_time_; }; // All segments have the same duration and size. -TEST_F(TimeShiftBufferDepthTest, Normal) { +TEST_P(TimeShiftBufferDepthTest, Normal) { const int kTimeShiftBufferDepth = 10; // 10 sec. mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; // Trick to make every segment 1 second long. const uint64_t kDuration = kDefaultTimeScale; const uint64_t kSize = 10000; @@ -753,7 +930,7 @@ TEST_F(TimeShiftBufferDepthTest, Normal) { CHECK_EQ(kDuration / kDefaultTimeScale * kRepeat, kLength); - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); // There should only be the last 11 segments because timeshift is 10 sec and // each segment is 1 sec and the latest segments start time is "current @@ -761,11 +938,12 @@ TEST_F(TimeShiftBufferDepthTest, Normal) { // depth. // Also note that S@r + 1 is the actual number of segments. const int kExpectedRepeatsLeft = kTimeShiftBufferDepth; - const std::string expected_s_element = base::StringPrintf( - kSElementTemplate, kDuration * (kRepeat - kExpectedRepeatsLeft), - kDuration, static_cast(kExpectedRepeatsLeft)); - const int kExpectedStartNumber = kRepeat - kExpectedRepeatsLeft + 1; + + const std::string expected_s_element = base::StringPrintf( + kSElementTemplate, + initial_start_time_ + kDuration * (kRepeat - kExpectedRepeatsLeft), + kDuration, static_cast(kExpectedRepeatsLeft)); EXPECT_THAT( representation_->GetXml().get(), XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber))); @@ -776,42 +954,38 @@ TEST_F(TimeShiftBufferDepthTest, Normal) { // For example if TimeShiftBufferDepth = 1 min. and a 10 min segment was just // added. Before that 9 min segment was added. The 9 min segment should not be // removed from the MPD. -TEST_F(TimeShiftBufferDepthTest, TimeShiftBufferDepthShorterThanSegmentLength) { +TEST_P(TimeShiftBufferDepthTest, TimeShiftBufferDepthShorterThanSegmentLength) { const int kTimeShiftBufferDepth = 10; // 10 sec. mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; // Each duration is a second longer than timeShiftBufferDepth. const uint64_t kDuration = kDefaultTimeScale * (kTimeShiftBufferDepth + 1); const uint64_t kSize = 10000; const uint64_t kRepeat = 1; - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); - // The two segments should be both present. const std::string expected_s_element = base::StringPrintf( - kSElementTemplate, kInitialStartTime, kDuration, kRepeat); - + kSElementTemplate, initial_start_time_, kDuration, kRepeat); EXPECT_THAT( representation_->GetXml().get(), XmlNodeEqual(ExpectedXml(expected_s_element, kDefaultStartNumber))); } // More generic version the normal test. -TEST_F(TimeShiftBufferDepthTest, Generic) { +TEST_P(TimeShiftBufferDepthTest, Generic) { const int kTimeShiftBufferDepth = 30; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 123; const uint64_t kDuration = kDefaultTimeScale; const uint64_t kSize = 10000; const uint64_t kRepeat = 1000; - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); const uint64_t first_s_element_end_time = - kInitialStartTime + kDuration * (kRepeat + 1); + initial_start_time_ + kDuration * (kRepeat + 1); // Now add 2 kTimeShiftBufferDepth long segments. const int kNumMoreSegments = 2; @@ -839,20 +1013,19 @@ TEST_F(TimeShiftBufferDepthTest, Generic) { // This should have all of the two-second segments and 60 one-second // segments. Note that it expects 60 segments from the first S element because // the most recent segment added does not count -TEST_F(TimeShiftBufferDepthTest, MoreThanOneS) { +TEST_P(TimeShiftBufferDepthTest, MoreThanOneS) { const int kTimeShiftBufferDepth = 100; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; const uint64_t kSize = 20000; const uint64_t kOneSecondDuration = kDefaultTimeScale; const uint64_t kOneSecondSegmentRepeat = 99; - AddSegments(kInitialStartTime, kOneSecondDuration, kSize, + AddSegments(initial_start_time_, kOneSecondDuration, kSize, kOneSecondSegmentRepeat); const uint64_t first_s_element_end_time = - kInitialStartTime + kOneSecondDuration * (kOneSecondSegmentRepeat + 1); + initial_start_time_ + kOneSecondDuration * (kOneSecondSegmentRepeat + 1); const uint64_t kTwoSecondDuration = 2 * kDefaultTimeScale; const uint64_t kTwoSecondSegmentRepeat = 20; @@ -864,7 +1037,8 @@ TEST_F(TimeShiftBufferDepthTest, MoreThanOneS) { kTimeShiftBufferDepth; std::string expected_s_element = base::StringPrintf( - kSElementTemplate, kOneSecondDuration * kExpectedRemovedSegments, + kSElementTemplate, + initial_start_time_ + kOneSecondDuration * kExpectedRemovedSegments, kOneSecondDuration, kOneSecondSegmentRepeat - kExpectedRemovedSegments); expected_s_element += base::StringPrintf(kSElementTemplate, first_s_element_end_time, @@ -884,20 +1058,19 @@ TEST_F(TimeShiftBufferDepthTest, MoreThanOneS) { // // and we add another contiguous 2 second segment. // Then the first S element's last segment should still be in the MPD. -TEST_F(TimeShiftBufferDepthTest, UseLastSegmentInS) { +TEST_P(TimeShiftBufferDepthTest, UseLastSegmentInS) { const int kTimeShiftBufferDepth = 9; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 1; const uint64_t kDuration1 = static_cast(kDefaultTimeScale * 1.5); const uint64_t kSize = 20000; const uint64_t kRepeat1 = 1; - AddSegments(kInitialStartTime, kDuration1, kSize, kRepeat1); + AddSegments(initial_start_time_, kDuration1, kSize, kRepeat1); const uint64_t first_s_element_end_time = - kInitialStartTime + kDuration1 * (kRepeat1 + 1); + initial_start_time_ + kDuration1 * (kRepeat1 + 1); const uint64_t kTwoSecondDuration = 2 * kDefaultTimeScale; const uint64_t kTwoSecondSegmentRepeat = 4; @@ -907,7 +1080,7 @@ TEST_F(TimeShiftBufferDepthTest, UseLastSegmentInS) { std::string expected_s_element = base::StringPrintf( kSElementTemplateWithoutR, - kInitialStartTime + kDuration1, // Expect one segment removed. + initial_start_time_ + kDuration1, // Expect one segment removed. kDuration1); expected_s_element += @@ -918,12 +1091,11 @@ TEST_F(TimeShiftBufferDepthTest, UseLastSegmentInS) { } // Gap between S elements but both should be included. -TEST_F(TimeShiftBufferDepthTest, NormalGap) { +TEST_P(TimeShiftBufferDepthTest, NormalGap) { const int kTimeShiftBufferDepth = 10; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; const uint64_t kDuration = kDefaultTimeScale; const uint64_t kSize = 20000; const uint64_t kRepeat = 6; @@ -932,16 +1104,16 @@ TEST_F(TimeShiftBufferDepthTest, NormalGap) { CHECK_LT(kRepeat - 1u, static_cast(kTimeShiftBufferDepth)); CHECK_EQ(kDuration, kDefaultTimeScale); - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); const uint64_t first_s_element_end_time = - kInitialStartTime + kDuration * (kRepeat + 1); + initial_start_time_ + kDuration * (kRepeat + 1); const uint64_t gap_s_element_start_time = first_s_element_end_time + 1; AddSegments(gap_s_element_start_time, kDuration, kSize, /* no repeat */ 0); std::string expected_s_element = base::StringPrintf( - kSElementTemplate, kInitialStartTime, kDuration, kRepeat); + kSElementTemplate, initial_start_time_, kDuration, kRepeat); expected_s_element += base::StringPrintf(kSElementTemplateWithoutR, gap_s_element_start_time, kDuration); @@ -951,19 +1123,18 @@ TEST_F(TimeShiftBufferDepthTest, NormalGap) { } // Case where there is a huge gap so the first S element is removed. -TEST_F(TimeShiftBufferDepthTest, HugeGap) { +TEST_P(TimeShiftBufferDepthTest, HugeGap) { const int kTimeShiftBufferDepth = 10; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; const uint64_t kDuration = kDefaultTimeScale; const uint64_t kSize = 20000; const uint64_t kRepeat = 6; - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); const uint64_t first_s_element_end_time = - kInitialStartTime + kDuration * (kRepeat + 1); + initial_start_time_ + kDuration * (kRepeat + 1); // Big enough gap so first S element should not be there. const uint64_t gap_s_element_start_time = @@ -987,34 +1158,34 @@ TEST_F(TimeShiftBufferDepthTest, HugeGap) { } // Check if startNumber is working correctly. -TEST_F(TimeShiftBufferDepthTest, ManySegments) { +TEST_P(TimeShiftBufferDepthTest, ManySegments) { const int kTimeShiftBufferDepth = 1; mutable_mpd_options()->mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - const uint64_t kInitialStartTime = 0; const uint64_t kDuration = kDefaultTimeScale; const uint64_t kSize = 20000; const uint64_t kRepeat = 10000; const uint64_t kTotalNumSegments = kRepeat + 1; - AddSegments(kInitialStartTime, kDuration, kSize, kRepeat); + AddSegments(initial_start_time_, kDuration, kSize, kRepeat); const int kExpectedSegmentsLeft = kTimeShiftBufferDepth + 1; const int kExpectedSegmentsRepeat = kExpectedSegmentsLeft - 1; const int kExpectedRemovedSegments = kTotalNumSegments - kExpectedSegmentsLeft; + const int kExpectedStartNumber = + kDefaultStartNumber + kExpectedRemovedSegments; std::string expected_s_element = base::StringPrintf( - kSElementTemplate, kExpectedRemovedSegments * kDuration, kDuration, + kSElementTemplate, + initial_start_time_ + kExpectedRemovedSegments * kDuration, kDuration, static_cast(kExpectedSegmentsRepeat)); - EXPECT_THAT( representation_->GetXml().get(), - XmlNodeEqual(ExpectedXml( - expected_s_element, kDefaultStartNumber + kExpectedRemovedSegments))); + XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber))); } -TEST_F(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) { +TEST_P(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) { const char kSegmentTemplate[] = "memory://$Number$.mp4"; const char kStringPrintTemplate[] = "memory://%d.mp4"; @@ -1092,4 +1263,8 @@ TEST_F(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) { kStringPrintTemplate, last_available_segment_index - 1))); } +INSTANTIATE_TEST_CASE_P(InitialStartTime, + TimeShiftBufferDepthTest, + Values(0, 1000)); + } // namespace shaka diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index 125ebbaaa3..058e13723d 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -6,6 +6,8 @@ #include "packager/mpd/base/xml/xml_node.h" +#include + #include #include @@ -17,6 +19,11 @@ #include "packager/mpd/base/mpd_utils.h" #include "packager/mpd/base/segment_info.h" +DEFINE_bool(segment_template_constant_duration, + false, + "Generates SegmentTemplate@duration if all segments except the " + "last one has the same duration if this flag is set to true."); + namespace shaka { using xml::XmlNode; @@ -31,16 +38,42 @@ std::string RangeToString(const Range& range) { base::Uint64ToString(range.end()); } +// Check if segments are continuous and all segments except the last one are of +// the same duration. +bool IsTimelineConstantDuration(const std::list& segment_infos, + uint32_t start_number) { + if (!FLAGS_segment_template_constant_duration) + return false; + + DCHECK(!segment_infos.empty()); + if (segment_infos.size() > 2) + return false; + + const SegmentInfo& first_segment = segment_infos.front(); + if (first_segment.start_time != first_segment.duration * (start_number - 1)) + return false; + + if (segment_infos.size() == 1) + return true; + + const SegmentInfo& last_segment = segment_infos.back(); + if (last_segment.repeat != 0) + return false; + + const uint64_t expected_last_segment_start_time = + first_segment.start_time + + first_segment.duration * (first_segment.repeat + 1); + return expected_last_segment_start_time == last_segment.start_time; +} + bool PopulateSegmentTimeline(const std::list& segment_infos, XmlNode* segment_timeline) { - for (std::list::const_iterator it = segment_infos.begin(); - it != segment_infos.end(); - ++it) { + for (const SegmentInfo& segment_info : segment_infos) { XmlNode s_element("S"); - s_element.SetIntegerAttribute("t", it->start_time); - s_element.SetIntegerAttribute("d", it->duration); - if (it->repeat > 0) - s_element.SetIntegerAttribute("r", it->repeat); + s_element.SetIntegerAttribute("t", segment_info.start_time); + s_element.SetIntegerAttribute("d", segment_info.duration); + if (segment_info.repeat > 0) + s_element.SetIntegerAttribute("r", segment_info.repeat); CHECK(segment_timeline->AddChild(s_element.PassScopedPtr())); } @@ -347,12 +380,21 @@ bool RepresentationXmlNode::AddLiveOnlyInfo( segment_template.SetIntegerAttribute("startNumber", start_number); } - // TODO(rkuroiwa): Find out when a live MPD doesn't require SegmentTimeline. - XmlNode segment_timeline("SegmentTimeline"); - - return PopulateSegmentTimeline(segment_infos, &segment_timeline) && - segment_template.AddChild(segment_timeline.PassScopedPtr()) && - AddChild(segment_template.PassScopedPtr()); + if (!segment_infos.empty()) { + // Don't use SegmentTimeline if all segments except the last one are of + // the same duration. + if (IsTimelineConstantDuration(segment_infos, start_number)) { + segment_template.SetIntegerAttribute("duration", + segment_infos.front().duration); + } else { + XmlNode segment_timeline("SegmentTimeline"); + if (!PopulateSegmentTimeline(segment_infos, &segment_timeline) || + !segment_template.AddChild(segment_timeline.PassScopedPtr())) { + return false; + } + } + } + return AddChild(segment_template.PassScopedPtr()); } bool RepresentationXmlNode::AddAudioChannelInfo(const AudioInfo& audio_info) { diff --git a/packager/mpd/base/xml/xml_node_unittest.cc b/packager/mpd/base/xml/xml_node_unittest.cc index 817df14907..2e164c9af5 100644 --- a/packager/mpd/base/xml/xml_node_unittest.cc +++ b/packager/mpd/base/xml/xml_node_unittest.cc @@ -4,6 +4,7 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +#include #include #include #include @@ -16,6 +17,8 @@ #include "packager/mpd/base/xml/xml_node.h" #include "packager/mpd/test/xml_compare.h" +DECLARE_bool(segment_template_constant_duration); + namespace shaka { namespace xml { @@ -191,5 +194,173 @@ TEST(XmlNodeTest, AddEC3AudioInfo) { "\n")); } +class LiveSegmentTimelineTest : public ::testing::Test { + protected: + void SetUp() override { + FLAGS_segment_template_constant_duration = true; + media_info_.set_segment_template_url("$Number$.m4s"); + } + + void TearDown() override { FLAGS_segment_template_constant_duration = false; } + + MediaInfo media_info_; +}; + +TEST_F(LiveSegmentTimelineTest, OneSegmentInfo) { + const uint32_t kStartNumber = 1; + const uint64_t kStartTime = 0; + const uint64_t kDuration = 100; + const uint64_t kRepeat = 9; + + std::list segment_infos = { + {kStartTime, kDuration, kRepeat}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT( + representation.GetRawPtr(), + XmlNodeEqual("" + " " + "")); +} + +TEST_F(LiveSegmentTimelineTest, OneSegmentInfoNonZeroStartTime) { + const uint32_t kStartNumber = 1; + const uint64_t kNonZeroStartTime = 500; + const uint64_t kDuration = 100; + const uint64_t kRepeat = 9; + + std::list segment_infos = { + {kNonZeroStartTime, kDuration, kRepeat}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT(representation.GetRawPtr(), + XmlNodeEqual( + "" + " " + " " + " " + " " + " " + "")); +} + +TEST_F(LiveSegmentTimelineTest, OneSegmentInfoMatchingStartTimeAndNumber) { + const uint32_t kStartNumber = 6; + const uint64_t kNonZeroStartTime = 500; + const uint64_t kDuration = 100; + const uint64_t kRepeat = 9; + + std::list segment_infos = { + {kNonZeroStartTime, kDuration, kRepeat}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT( + representation.GetRawPtr(), + XmlNodeEqual("" + " " + "")); +} + +TEST_F(LiveSegmentTimelineTest, AllSegmentsSameDurationExpectLastOne) { + const uint32_t kStartNumber = 1; + + const uint64_t kStartTime1 = 0; + const uint64_t kDuration1 = 100; + const uint64_t kRepeat1 = 9; + + const uint64_t kStartTime2 = kStartTime1 + (kRepeat1 + 1) * kDuration1; + const uint64_t kDuration2 = 200; + const uint64_t kRepeat2 = 0; + + std::list segment_infos = { + {kStartTime1, kDuration1, kRepeat1}, + {kStartTime2, kDuration2, kRepeat2}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT( + representation.GetRawPtr(), + XmlNodeEqual("" + " " + "")); +} + +TEST_F(LiveSegmentTimelineTest, SecondSegmentInfoNonZeroRepeat) { + const uint32_t kStartNumber = 1; + + const uint64_t kStartTime1 = 0; + const uint64_t kDuration1 = 100; + const uint64_t kRepeat1 = 9; + + const uint64_t kStartTime2 = kStartTime1 + (kRepeat1 + 1) * kDuration1; + const uint64_t kDuration2 = 200; + const uint64_t kRepeat2 = 1; + + std::list segment_infos = { + {kStartTime1, kDuration1, kRepeat1}, + {kStartTime2, kDuration2, kRepeat2}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT(representation.GetRawPtr(), + XmlNodeEqual( + "" + " " + " " + " " + " " + " " + " " + "")); +} + +TEST_F(LiveSegmentTimelineTest, TwoSegmentInfoWithGap) { + const uint32_t kStartNumber = 1; + + const uint64_t kStartTime1 = 0; + const uint64_t kDuration1 = 100; + const uint64_t kRepeat1 = 9; + + const uint64_t kGap = 100; + const uint64_t kStartTime2 = kGap + kStartTime1 + (kRepeat1 + 1) * kDuration1; + const uint64_t kDuration2 = 200; + const uint64_t kRepeat2 = 0; + + std::list segment_infos = { + {kStartTime1, kDuration1, kRepeat1}, + {kStartTime2, kDuration2, kRepeat2}, + }; + RepresentationXmlNode representation; + ASSERT_TRUE( + representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber)); + + EXPECT_THAT(representation.GetRawPtr(), + XmlNodeEqual( + "" + " " + " " + " " + " " + " " + " " + "")); +} + } // namespace xml } // namespace shaka diff --git a/packager/mpd/mpd.gyp b/packager/mpd/mpd.gyp index 10156a3b02..a89a195652 100644 --- a/packager/mpd/mpd.gyp +++ b/packager/mpd/mpd.gyp @@ -104,6 +104,7 @@ '../media/test/media_test.gyp:run_tests_with_atexit_manager', '../testing/gmock.gyp:gmock', '../testing/gtest.gyp:gtest', + '../third_party/gflags/gflags.gyp:gflags', 'mpd_builder', 'mpd_mocks', 'mpd_util', diff --git a/packager/mpd/public/mpd_params.h b/packager/mpd/public/mpd_params.h index 20ecd6152c..b92d5d447d 100644 --- a/packager/mpd/public/mpd_params.h +++ b/packager/mpd/public/mpd_params.h @@ -59,6 +59,16 @@ struct MpdParams { bool generate_static_live_mpd = false; /// Try to generate DASH-IF IOP compliant MPD. bool generate_dash_if_iop_compliant_mpd = true; + /// For live profile only. + /// If enabled, segments with close duration (i.e. with difference less than + /// one sample) are considered to have the same duration. This enables + /// MPD generator to generate less SegmentTimeline entries. If all segments + /// are of the same duration except the last one, we will do further + /// optimization to use SegmentTemplate@duration instead and omit + /// SegmentTimeline completely. + /// Ignored if $Time$ is used in segment template, since $Time$ requires + /// accurate Segment Timeline. + bool allow_approximate_segment_timeline = false; }; } // namespace shaka diff --git a/packager/packager.cc b/packager/packager.cc index 8c7341a6a8..8295a60fe9 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -858,8 +858,10 @@ Status Packager::Initialize( if (!mpd_params.mpd_output.empty()) { const bool on_demand_dash_profile = stream_descriptors.begin()->segment_template.empty(); - const MpdOptions mpd_options = - media::GetMpdOptions(on_demand_dash_profile, mpd_params); + const double target_segment_duration = + packaging_params.chunking_params.segment_duration_in_seconds; + const MpdOptions mpd_options = media::GetMpdOptions( + on_demand_dash_profile, mpd_params, target_segment_duration); internal->mpd_notifier.reset(new SimpleMpdNotifier(mpd_options)); if (!internal->mpd_notifier->Init()) { LOG(ERROR) << "MpdNotifier failed to initialize.";