From 37d4ff017f16163bbdd467a6b121968b5c40c97e Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Tue, 17 Apr 2018 15:03:02 -0700 Subject: [PATCH] Support removing segments outside of live window in HLS Issue: #223 Change-Id: Ib91c60268d8df9adbaf5f6cac77eaebd6a3edb6e --- packager/hls/base/media_playlist.cc | 29 +++- packager/hls/base/media_playlist.h | 9 +- packager/hls/base/media_playlist_unittest.cc | 92 ++++++++++- packager/hls/public/hls_params.h | 7 + packager/mpd/base/representation.cc | 1 + packager/mpd/base/representation.h | 3 +- packager/mpd/base/representation_unittest.cc | 160 ++++++++++--------- 7 files changed, 216 insertions(+), 85 deletions(-) diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index 3444692d1c..a0970793b3 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -18,6 +18,7 @@ #include "packager/file/file.h" #include "packager/hls/base/tag.h" #include "packager/media/base/language_utils.h" +#include "packager/media/base/muxer_util.h" #include "packager/version/version.h" namespace shaka { @@ -577,7 +578,6 @@ void MediaPlaylist::SlideWindow() { HlsEntry::EntryType prev_entry_type = HlsEntry::EntryType::kExtInf; std::list>::iterator last = entries_.begin(); - size_t num_segments_removed = 0; for (; last != entries_.end(); ++last) { HlsEntry::EntryType entry_type = last->get()->type(); if (entry_type == HlsEntry::EntryType::kExtKey) { @@ -588,13 +588,14 @@ void MediaPlaylist::SlideWindow() { ++discontinuity_sequence_number_; } else { DCHECK_EQ(entry_type, HlsEntry::EntryType::kExtInf); - const SegmentInfoEntry* segment_info = - reinterpret_cast(last->get()); + const SegmentInfoEntry& segment_info = + *reinterpret_cast(last->get()); const double last_segment_end_time = - segment_info->start_time() + segment_info->duration(); + segment_info.start_time() + segment_info.duration(); if (timeshift_limit < last_segment_end_time) break; - ++num_segments_removed; + RemoveOldSegment(segment_info.start_time()); + media_sequence_number_++; } prev_entry_type = entry_type; } @@ -602,7 +603,23 @@ void MediaPlaylist::SlideWindow() { // Add key entries back. entries_.insert(entries_.begin(), std::make_move_iterator(ext_x_keys.begin()), std::make_move_iterator(ext_x_keys.end())); - media_sequence_number_ += num_segments_removed; +} + +void MediaPlaylist::RemoveOldSegment(uint64_t start_time) { + if (hls_params_.preserved_segments_outside_live_window == 0) + return; + if (stream_type_ == MediaPlaylistStreamType::kVideoIFramesOnly) + return; + + segments_to_be_removed_.push_back( + media::GetSegmentName(media_info_.segment_template(), start_time, + media_sequence_number_, media_info_.bandwidth())); + while (segments_to_be_removed_.size() > + hls_params_.preserved_segments_outside_live_window) { + VLOG(2) << "Deleting " << segments_to_be_removed_.front(); + File::Delete(segments_to_be_removed_.front().c_str()); + segments_to_be_removed_.pop_front(); + } } } // namespace hls diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h index 8682185da4..d256408f91 100644 --- a/packager/hls/base/media_playlist.h +++ b/packager/hls/base/media_playlist.h @@ -188,8 +188,12 @@ class MediaPlaylist { // Remove elements from |entries_| for live profile. Increments // |sequence_number_| by the number of segments removed. void SlideWindow(); + // Remove the segment specified by |start_time|. The actual deletion can + // happen at a later time depending on the value of + // |preserved_segment_outside_live_window| in |hls_params_|. + void RemoveOldSegment(uint64_t start_time); - const HlsParams hls_params_; + const HlsParams& hls_params_; // Mainly for MasterPlaylist to use these values. const std::string file_name_; const std::string name_; @@ -218,6 +222,9 @@ class MediaPlaylist { uint32_t target_duration_ = 0; std::list> entries_; + // A list to hold the file names of the segments to be removed temporarily. + // Once a file is actually removed, it is removed from the list. + std::list segments_to_be_removed_; // Used by kVideoIFrameOnly playlists to track the i-frames (key frames). struct KeyFrameInfo { diff --git a/packager/hls/base/media_playlist_unittest.cc b/packager/hls/base/media_playlist_unittest.cc index be214d9f49..257df1a9af 100644 --- a/packager/hls/base/media_playlist_unittest.cc +++ b/packager/hls/base/media_playlist_unittest.cc @@ -7,6 +7,9 @@ #include #include +#include "packager/base/strings/stringprintf.h" +#include "packager/file/file.h" +#include "packager/file/file_closer.h" #include "packager/file/file_test_util.h" #include "packager/hls/base/media_playlist.h" #include "packager/version/version.h" @@ -42,10 +45,9 @@ class MediaPlaylistTest : public ::testing::Test { : default_file_name_(kDefaultPlaylistFileName), default_name_("default_name"), default_group_id_("default_group_id") { - HlsParams hls_params; - hls_params.playlist_type = type; - hls_params.time_shift_buffer_depth = kTimeShiftBufferDepth; - media_playlist_.reset(new MediaPlaylist(hls_params, default_file_name_, + hls_params_.playlist_type = type; + hls_params_.time_shift_buffer_depth = kTimeShiftBufferDepth; + media_playlist_.reset(new MediaPlaylist(hls_params_, default_file_name_, default_name_, default_group_id_)); } @@ -65,9 +67,12 @@ class MediaPlaylistTest : public ::testing::Test { valid_video_media_info_.set_reference_time_scale(kTimeScale); } + HlsParams* mutable_hls_params() { return &hls_params_; } + const std::string default_file_name_; const std::string default_name_; const std::string default_group_id_; + HlsParams hls_params_; std::unique_ptr media_playlist_; MediaInfo valid_video_media_info_; @@ -870,5 +875,84 @@ TEST_F(IFrameMediaPlaylistTest, MultiSegment) { ASSERT_FILE_STREQ(kMemoryFilePath, kExpectedOutput); } +namespace { +const int kNumPreservedSegmentsOutsideLiveWindow = 3; +const int kMaxNumSegmentsAvailable = + kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow; + +const char kSegmentTemplate[] = "memory://$Number$.mp4"; +const char kSegmentTemplateUrl[] = "video/$Number$.mp4"; +const char kStringPrintTemplate[] = "memory://%d.mp4"; +const char kIgnoredSegmentName[] = "ignored_segment_name"; + +const uint64_t kInitialStartTime = 0; +const uint64_t kDuration = kTimeScale; +} // namespace + +class MediaPlaylistDeleteSegmentsTest : public LiveMediaPlaylistTest { + public: + void SetUp() override { + LiveMediaPlaylistTest::SetUp(); + + // Create 100 files with the template. + for (int i = 1; i <= 100; ++i) { + File::WriteStringToFile( + base::StringPrintf(kStringPrintTemplate, i).c_str(), "dummy content"); + } + + valid_video_media_info_.set_segment_template(kSegmentTemplate); + valid_video_media_info_.set_segment_template_url(kSegmentTemplateUrl); + ASSERT_TRUE(media_playlist_->SetMediaInfo(valid_video_media_info_)); + + mutable_hls_params()->preserved_segments_outside_live_window = + kNumPreservedSegmentsOutsideLiveWindow; + } + + bool SegmentDeleted(const std::string& segment_name) { + std::unique_ptr file_closer( + File::Open(segment_name.c_str(), "r")); + return file_closer.get() == nullptr; + } +}; + +// Verify that no segments are deleted initially until there are more than +// |kMaxNumSegmentsAvailable| segments. +TEST_F(MediaPlaylistDeleteSegmentsTest, NoSegmentsDeletedInitially) { + for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { + media_playlist_->AddSegment(kIgnoredSegmentName, + kInitialStartTime + i * kDuration, kDuration, + kZeroByteOffset, kMBytes); + } + for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { + EXPECT_FALSE( + SegmentDeleted(base::StringPrintf(kStringPrintTemplate, i + 1))); + } +} + +TEST_F(MediaPlaylistDeleteSegmentsTest, OneSegmentDeleted) { + for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) { + media_playlist_->AddSegment(kIgnoredSegmentName, + kInitialStartTime + i * kDuration, kDuration, + kZeroByteOffset, kMBytes); + } + EXPECT_FALSE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 2))); + EXPECT_TRUE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 1))); +} + +TEST_F(MediaPlaylistDeleteSegmentsTest, ManySegments) { + int many_segments = 50; + for (int i = 0; i < many_segments; ++i) { + media_playlist_->AddSegment(kIgnoredSegmentName, + kInitialStartTime + i * kDuration, kDuration, + kZeroByteOffset, kMBytes); + } + const int last_available_segment_index = + many_segments - kMaxNumSegmentsAvailable + 1; + EXPECT_FALSE(SegmentDeleted( + base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); + EXPECT_TRUE(SegmentDeleted(base::StringPrintf( + kStringPrintTemplate, last_available_segment_index - 1))); +} + } // namespace hls } // namespace shaka diff --git a/packager/hls/public/hls_params.h b/packager/hls/public/hls_params.h index fa21786cef..6d2cc489e6 100644 --- a/packager/hls/public/hls_params.h +++ b/packager/hls/public/hls_params.h @@ -31,6 +31,13 @@ struct HlsParams { /// Defines the live window, or the guaranteed duration of the time shifting /// buffer for 'live' playlists. double time_shift_buffer_depth = 0; + /// Segments outside live window (defined by 'time_shift_buffer_depth' above) + /// are automatically removed except the latest number of segments defined by + /// this parameter. This is needed to accommodate latencies in various stages + /// of content serving pipeline, so that the segments stay accessible as they + /// may still be accessed by the player. + /// The segments are not removed if the value is zero. + size_t preserved_segments_outside_live_window = 0; /// Defines the key uri for "identity" and "com.apple.streamingkeydelivery" /// key formats. Ignored if the playlist is not encrypted or not using the /// above key formats. diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index ebee94b55c..6227b0a8bc 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -498,6 +498,7 @@ void Representation::RemoveSegments(uint64_t start_time, } while (segments_to_be_removed_.size() > mpd_options_.mpd_params.preserved_segments_outside_live_window) { + VLOG(2) << "Deleting " << segments_to_be_removed_.front(); File::Delete(segments_to_be_removed_.front().c_str()); segments_to_be_removed_.pop_front(); } diff --git a/packager/mpd/base/representation.h b/packager/mpd/base/representation.h index 0cdd8f7a75..fef1ca3aa4 100644 --- a/packager/mpd/base/representation.h +++ b/packager/mpd/base/representation.h @@ -212,7 +212,8 @@ class Representation { std::list content_protection_elements_; // TODO(kqyang): Address sliding window issue with multiple periods. std::list segment_infos_; - // A temporary list to hold the file names of segments to be removed. + // A list to hold the file names of the segments to be removed temporarily. + // Once a file is actually removed, it is removed from the list. std::list segments_to_be_removed_; const uint32_t id_; diff --git a/packager/mpd/base/representation_unittest.cc b/packager/mpd/base/representation_unittest.cc index 16b5cb0785..f25e453da8 100644 --- a/packager/mpd/base/representation_unittest.cc +++ b/packager/mpd/base/representation_unittest.cc @@ -27,12 +27,6 @@ namespace { const uint32_t kAnyRepresentationId = 1; -bool SegmentDeleted(const std::string& segment_name) { - std::unique_ptr file_closer( - File::Open(segment_name.c_str(), "r")); - return file_closer.get() == nullptr; -} - class MockRepresentationStateChangeListener : public RepresentationStateChangeListener { public: @@ -1185,86 +1179,106 @@ TEST_P(TimeShiftBufferDepthTest, ManySegments) { XmlNodeEqual(ExpectedXml(expected_s_element, kExpectedStartNumber))); } -TEST_P(TimeShiftBufferDepthTest, DeleteSegmentsOutsideOfLiveWindow) { - const char kSegmentTemplate[] = "memory://$Number$.mp4"; - const char kStringPrintTemplate[] = "memory://%d.mp4"; +INSTANTIATE_TEST_CASE_P(InitialStartTime, + TimeShiftBufferDepthTest, + Values(0, 1000)); - // Create 100 files with the template. - for (int i = 1; i <= 100; ++i) { - File::WriteStringToFile(base::StringPrintf(kStringPrintTemplate, i).c_str(), - "dummy content"); +namespace { +const int kTimeShiftBufferDepth = 2; +const int kNumPreservedSegmentsOutsideLiveWindow = 3; +const int kMaxNumSegmentsAvailable = + kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow; + +const char kSegmentTemplate[] = "memory://$Number$.mp4"; +const char kSegmentTemplateUrl[] = "video/$Number$.mp4"; +const char kStringPrintTemplate[] = "memory://%d.mp4"; + +const uint64_t kInitialStartTime = 0; +const uint64_t kDuration = kDefaultTimeScale; +const uint64_t kSize = 10; +const uint64_t kNoRepeat = 0; +} // namespace + +class RepresentationDeleteSegmentsTest : public SegmentTimelineTestBase { + public: + void SetUp() override { + SegmentTimelineTestBase::SetUp(); + + // Create 100 files with the template. + for (int i = 1; i <= 100; ++i) { + File::WriteStringToFile( + base::StringPrintf(kStringPrintTemplate, i).c_str(), "dummy content"); + } + + MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); + media_info.set_segment_template(kSegmentTemplate); + media_info.set_segment_template_url(kSegmentTemplateUrl); + representation_ = + CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); + ASSERT_TRUE(representation_->Init()); + + mpd_options_.mpd_params.time_shift_buffer_depth = kTimeShiftBufferDepth; + mpd_options_.mpd_params.preserved_segments_outside_live_window = + kNumPreservedSegmentsOutsideLiveWindow; } - MediaInfo media_info = ConvertToMediaInfo(GetDefaultMediaInfo()); - media_info.set_segment_template(kSegmentTemplate); - representation_ = - CreateRepresentation(media_info, kAnyRepresentationId, NoListener()); - ASSERT_TRUE(representation_->Init()); + bool SegmentDeleted(const std::string& segment_name) { + std::unique_ptr file_closer( + File::Open(segment_name.c_str(), "r")); + return file_closer.get() == nullptr; + } +}; - const int kTimeShiftBufferDepth = 2; - const int kNumPreservedSegmentsOutsideLiveWindow = 3; - const int kMaxNumSegmentsAvailable = - kTimeShiftBufferDepth + 1 + kNumPreservedSegmentsOutsideLiveWindow; - - mutable_mpd_options()->mpd_params.time_shift_buffer_depth = - kTimeShiftBufferDepth; - mutable_mpd_options()->mpd_params.preserved_segments_outside_live_window = - kNumPreservedSegmentsOutsideLiveWindow; - - const uint64_t kInitialStartTime = 0; - const uint64_t kDuration = kDefaultTimeScale; - const uint64_t kSize = 10; - const uint64_t kNoRepeat = 0; - - // Verify that no segments are deleted initially until there are more than - // |kMaxNumSegmentsAvailable| segments. - uint64_t next_start_time = kInitialStartTime; - int num_total_segments = 0; - int last_available_segment_index = 1; // No segments are deleted. +// Verify that no segments are deleted initially until there are more than +// |kMaxNumSegmentsAvailable| segments. +TEST_F(RepresentationDeleteSegmentsTest, NoSegmentsDeletedInitially) { for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { - AddSegments(next_start_time, kDuration, kSize, kNoRepeat); - next_start_time += kDuration; - ++num_total_segments; - - EXPECT_FALSE(SegmentDeleted(base::StringPrintf( - kStringPrintTemplate, last_available_segment_index))); + AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat); } + for (int i = 0; i < kMaxNumSegmentsAvailable; ++i) { + EXPECT_FALSE( + SegmentDeleted(base::StringPrintf(kStringPrintTemplate, i + 1))); + } +} - AddSegments(next_start_time, kDuration, kSize, kNoRepeat); - next_start_time += kDuration; - ++num_total_segments; - last_available_segment_index = 2; - - EXPECT_FALSE(SegmentDeleted( - base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); - EXPECT_TRUE(SegmentDeleted(base::StringPrintf( - kStringPrintTemplate, last_available_segment_index - 1))); - - // Verify that there are exactly |kMaxNumSegmentsAvailable| segments - // remaining. - const uint64_t kRepeat = 10; - AddSegments(next_start_time, kDuration, kSize, kRepeat); - next_start_time += (kRepeat + 1) * kDuration; - num_total_segments += kRepeat + 1; - last_available_segment_index = - num_total_segments - kMaxNumSegmentsAvailable + 1; - - EXPECT_FALSE(SegmentDeleted( - base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); - EXPECT_TRUE(SegmentDeleted(base::StringPrintf( - kStringPrintTemplate, last_available_segment_index - 1))); - - AddSegments(next_start_time, kDuration, kSize, kNoRepeat); - ++last_available_segment_index; +TEST_F(RepresentationDeleteSegmentsTest, OneSegmentDeleted) { + for (int i = 0; i <= kMaxNumSegmentsAvailable; ++i) { + AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat); + } + EXPECT_FALSE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 2))); + EXPECT_TRUE(SegmentDeleted(base::StringPrintf(kStringPrintTemplate, 1))); +} +// Verify that segments are deleted as expected with many non-repeating +// segments. +TEST_F(RepresentationDeleteSegmentsTest, ManyNonRepeatingSegments) { + int many_segments = 50; + for (int i = 0; i < many_segments; ++i) { + AddSegments(kInitialStartTime + i * kDuration, kDuration, kSize, kNoRepeat); + } + const int last_available_segment_index = + many_segments - kMaxNumSegmentsAvailable + 1; EXPECT_FALSE(SegmentDeleted( base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); EXPECT_TRUE(SegmentDeleted(base::StringPrintf( kStringPrintTemplate, last_available_segment_index - 1))); } -INSTANTIATE_TEST_CASE_P(InitialStartTime, - TimeShiftBufferDepthTest, - Values(0, 1000)); +// Verify that segments are deleted as expected with many repeating segments. +TEST_F(RepresentationDeleteSegmentsTest, ManyRepeatingSegments) { + const int kLoops = 4; + const uint64_t kRepeat = 10; + for (int i = 0; i < kLoops; ++i) { + AddSegments(kInitialStartTime + i * kDuration * (kRepeat + 1), kDuration, + kSize, kRepeat); + } + const int kNumSegments = kLoops * (kRepeat + 1); + const int last_available_segment_index = + kNumSegments - kMaxNumSegmentsAvailable + 1; + EXPECT_FALSE(SegmentDeleted( + base::StringPrintf(kStringPrintTemplate, last_available_segment_index))); + EXPECT_TRUE(SegmentDeleted(base::StringPrintf( + kStringPrintTemplate, last_available_segment_index - 1))); +} } // namespace shaka