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
This commit is contained in:
KongQun Yang 2017-09-25 12:18:50 -07:00
parent 902dfb905a
commit 224b597b48
17 changed files with 631 additions and 140 deletions

View File

@ -48,3 +48,17 @@ DASH options
Any audio/text tracks tagged with this language will have
<Role ... value=\"main\" /> 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.

View File

@ -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.");

View File

@ -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_

View File

@ -420,6 +420,8 @@ base::Optional<PackagingParams> 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)) {

View File

@ -181,7 +181,9 @@ std::unique_ptr<KeySource> 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;
}

View File

@ -45,7 +45,9 @@ std::unique_ptr<KeySource> 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

View File

@ -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.

View File

@ -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

View File

@ -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(

View File

@ -8,6 +8,8 @@
#include <gflags/gflags.h>
#include <algorithm>
#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<double>(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,30 +350,32 @@ 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);
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);
if (previous_segment_end_time == start_time &&
segment_infos_.back().duration == duration) {
return true;
// 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});
}
// 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;
return;
}
// A gap since previous.
@ -379,7 +387,6 @@ bool Representation::IsContiguous(uint64_t start_time,
<< "). The new segment starts at " << start_time
<< " but the previous segment ends at "
<< previous_segment_end_time << ".";
return false;
}
// No overlapping segments.
@ -388,11 +395,42 @@ bool Representation::IsContiguous(uint64_t start_time,
<< "Segments should not be overlapping. The new segment starts at "
<< start_time << " but the previous segment ends at "
<< previous_segment_end_time << ".";
return false;
}
}
// Within rounding error grace but technically not contiguous in terms of MPD.
return false;
segment_infos_.push_back({start_time, adjusted_duration, kNoRepeat});
}
bool Representation::ApproximiatelyEqual(uint64_t time1, uint64_t time2) const {
if (!allow_approximate_segment_timeline_)
return time1 == time2;
// 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.
// 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<uint32_t>(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() {

View File

@ -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<RepresentationStateChangeListener> 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

View File

@ -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[] =
"<S t=\"%" PRIu64 "\" d=\"%" PRIu64 "\"/>\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[] =
"<Representation id=\"1\" bandwidth=\"102400\" "
" codecs=\"avc1.010101\" mimeType=\"video/mp4\" sar=\"1:1\" "
" width=\"720\" height=\"480\" frameRate=\"10/5\">\n"
" <SegmentTemplate timescale=\"1000\" "
" initialization=\"init.mp4\" media=\"$Time$.mp4\" startNumber=\"1\">\n"
" <SegmentTimeline>\n"
" <S t=\"0\" d=\"10\"/>\n"
" </SegmentTimeline>\n"
" </SegmentTemplate>\n"
"</Representation>\n";
EXPECT_THAT(representation_->GetXml().get(), XmlNodeEqual(kExpectedXml));
expected_s_elements_ = "<S t=\"0\" d=\"10\"/>";
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"
" <SegmentTemplate timescale=\"1000\" initialization=\"init.mp4\" "
" media=\"$Number$.mp4\" startNumber=\"2\">\n"
" <SegmentTimeline/>\n"
" </SegmentTemplate>\n"
"</Representation>\n";
EXPECT_THAT(cloned_representation->GetXml().get(),
@ -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<double>(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<bool> {
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 =
"<S t=\"0\" d=\"10\"/>"
"<S t=\"10\" d=\"12\" r=\"1\"/>";
} 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<uint64_t>(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<uint64_t>(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<uint64_t> {
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<uint64_t>(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<uint64_t>(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) {
// <S t=3 d=2 r=3 />
// 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<uint64_t>(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<uint64_t>(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<uint64_t>(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

View File

@ -6,6 +6,8 @@
#include "packager/mpd/base/xml/xml_node.h"
#include <gflags/gflags.h>
#include <limits>
#include <set>
@ -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<SegmentInfo>& 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<SegmentInfo>& segment_infos,
XmlNode* segment_timeline) {
for (std::list<SegmentInfo>::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.
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");
return PopulateSegmentTimeline(segment_infos, &segment_timeline) &&
segment_template.AddChild(segment_timeline.PassScopedPtr()) &&
AddChild(segment_template.PassScopedPtr());
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) {

View File

@ -4,6 +4,7 @@
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include <gflags/gflags.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <libxml/tree.h>
@ -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) {
"</Representation>\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<SegmentInfo> segment_infos = {
{kStartTime, kDuration, kRepeat},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(
representation.GetRawPtr(),
XmlNodeEqual("<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" "
" startNumber=\"1\" duration=\"100\"/>"
"</Representation>"));
}
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<SegmentInfo> segment_infos = {
{kNonZeroStartTime, kDuration, kRepeat},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(representation.GetRawPtr(),
XmlNodeEqual(
"<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" startNumber=\"1\">"
" <SegmentTimeline>"
" <S t=\"500\" d=\"100\" r=\"9\"/>"
" </SegmentTimeline>"
" </SegmentTemplate>"
"</Representation>"));
}
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<SegmentInfo> segment_infos = {
{kNonZeroStartTime, kDuration, kRepeat},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(
representation.GetRawPtr(),
XmlNodeEqual("<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" "
" startNumber=\"6\" duration=\"100\"/>"
"</Representation>"));
}
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<SegmentInfo> segment_infos = {
{kStartTime1, kDuration1, kRepeat1},
{kStartTime2, kDuration2, kRepeat2},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(
representation.GetRawPtr(),
XmlNodeEqual("<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" "
" startNumber=\"1\" duration=\"100\"/>"
"</Representation>"));
}
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<SegmentInfo> segment_infos = {
{kStartTime1, kDuration1, kRepeat1},
{kStartTime2, kDuration2, kRepeat2},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(representation.GetRawPtr(),
XmlNodeEqual(
"<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" startNumber=\"1\">"
" <SegmentTimeline>"
" <S t=\"0\" d=\"100\" r=\"9\"/>"
" <S t=\"1000\" d=\"200\" r=\"1\"/>"
" </SegmentTimeline>"
" </SegmentTemplate>"
"</Representation>"));
}
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<SegmentInfo> segment_infos = {
{kStartTime1, kDuration1, kRepeat1},
{kStartTime2, kDuration2, kRepeat2},
};
RepresentationXmlNode representation;
ASSERT_TRUE(
representation.AddLiveOnlyInfo(media_info_, segment_infos, kStartNumber));
EXPECT_THAT(representation.GetRawPtr(),
XmlNodeEqual(
"<Representation>"
" <SegmentTemplate media=\"$Number$.m4s\" startNumber=\"1\">"
" <SegmentTimeline>"
" <S t=\"0\" d=\"100\" r=\"9\"/>"
" <S t=\"1100\" d=\"200\"/>"
" </SegmentTimeline>"
" </SegmentTemplate>"
"</Representation>"));
}
} // namespace xml
} // namespace shaka

View File

@ -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',

View File

@ -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

View File

@ -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.";